review: add sandbox wrappers and docs for AI review agents Add bwrap and firejail wrapper scripts that sandbox review agents against prompt injection in untrusted patches. Document agent configuration including Cursor CLI and sandboxing. Signed-off-by: Michael S. Tsirkin <mst@redhat.com>
diff --git a/docs/maintainer/review.rst b/docs/maintainer/review.rst index c1528d3..2b3ca10 100644 --- a/docs/maintainer/review.rst +++ b/docs/maintainer/review.rst
@@ -1105,13 +1105,19 @@ To use Gemini CLI instead:: [b4] - review-agent-command = gemini --sandbox --allowed-tools 'Bash(git:*) Read Glob Grep Write(.git/b4-review/**) Edit(.git/b4-review/**)' + review-agent-command = gemini --yolo review-agent-prompt-path = .git/agent-reviewer.md To use OpenAI Codex CLI:: [b4] - review-agent-command = codex --sandbox workspace-write + review-agent-command = codex exec --full-auto + review-agent-prompt-path = .git/agent-reviewer.md + +To use Cursor CLI:: + + [b4] + review-agent-command = cursor-agent --yolo review-agent-prompt-path = .git/agent-reviewer.md To use GitHub Copilot CLI:: @@ -1122,11 +1128,11 @@ .. note:: - Only **Claude Code** and **OpenAI Codex CLI** have been directly - tested. The command-line examples for Gemini CLI and GitHub Copilot - CLI are best-effort suggestions. If you get another tool working - (or find corrections for the above), please send your findings to - tools@kernel.org. + **Claude Code**, **OpenAI Codex CLI**, **Cursor CLI**, and + **Gemini CLI** have been tested. The command-line example for + GitHub Copilot CLI is a best-effort suggestion. If you get another + tool working (or find corrections for the above), please send your + findings to tools@kernel.org. The command is run from the repository top-level directory. The prompt file should contain instructions telling the agent how to review patches @@ -1135,6 +1141,44 @@ repository and adapt it to your project's coding standards and review guidelines. +Sandboxing the agent +^^^^^^^^^^^^^^^^^^^^ +Since the agent reviews untrusted patches, a prompt-injection attack +could trick it into running malicious commands. Two sandbox wrapper +scripts are included in ``misc/``. Both create a temporary HOME +containing only agent auth config (SSH keys, GPG keys, and cloud +credentials are never exposed), make the repository read-only, and +allow writes only to ``.git/b4-review/``. + +**bubblewrap** (recommended) -- ``misc/bwrap-review-agent.sh`` +replaces the real HOME entirely so credentials are invisible, not +merely read-only:: + + [b4] + review-agent-command = misc/bwrap-review-agent.sh cursor-agent --yolo + review-agent-prompt-path = .git/agent-reviewer.md + +**firejail** -- ``misc/firejail-review-agent.sh`` makes the +repository and real HOME read-only, with HOME env redirected to the +sandbox copy. The real HOME is still visible (read-only), so this +provides weaker isolation than bubblewrap:: + + [b4] + review-agent-command = misc/firejail-review-agent.sh cursor-agent --yolo + review-agent-prompt-path = .git/agent-reviewer.md + +Both wrappers are agent-agnostic -- they pass all arguments through +to the sandboxed command. If the required tool is not installed, they +fall back to running the agent without a sandbox. + +.. warning:: + + Do **not** use agents' built-in sandbox flags (e.g. ``gemini -s``, + ``codex --sandbox``) together with either wrapper. The agent's + sandbox creates a nested mount namespace that can override the + wrapper's read-only protections. Let the wrapper handle sandboxing + alone. + .. _customising_theme: Customising the colour theme
diff --git a/misc/bwrap-review-agent.sh b/misc/bwrap-review-agent.sh new file mode 100755 index 0000000..460a21d --- /dev/null +++ b/misc/bwrap-review-agent.sh
@@ -0,0 +1,70 @@ +#!/bin/bash +# bwrap-review-agent.sh -- bubblewrap sandbox for b4 review agents +# +# Full filesystem isolation: the entire filesystem is mounted +# read-only, real HOME is replaced with a temporary copy containing +# only agent auth config, and .git/b4-review/ is the sole writable +# path. Agent binaries installed under HOME (npm, pip, cargo) are +# re-exposed read-only. +# +# Requires: bubblewrap (bwrap) +# +# Usage -- set as your review-agent-command in git config: +# +# [b4] +# review-agent-command = misc/bwrap-review-agent.sh cursor-agent --yolo +# review-agent-prompt-path = .git/agent-reviewer.md + +set -euo pipefail + +mydir="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=review-agent-sandbox-lib.sh +. "$mydir/review-agent-sandbox-lib.sh" + +if ! command -v bwrap >/dev/null 2>&1; then + echo >&2 "bwrap-review-agent: bwrap not found, running agent without sandbox" + export HOME="$sandbox_home" + exec "$@" +fi + +# Mount layout (later mounts override earlier ones): +# +# / read-only entire host filesystem +# /dev read-write minimal private devtmpfs +# /proc read-only procfs +# /tmp read-write private tmpfs (empty, ephemeral) +# $real_home read-write sandbox_home replaces real HOME; +# ~/.ssh, ~/.gnupg, etc. are invisible +# $real_home/.local/ read-only agent binaries (pip, pipx installs) +# $real_home/.npm-*/ read-only agent binaries (npm global installs) +# $real_home/.cargo/ read-only agent binaries (cargo installs) +# $real_home/.nvm|... read-only node version managers +# $topdir read-only the git repository +# $review_dir read-write .git/b4-review/ -- review output +# +bwrap_args=( + --ro-bind / / # base: everything read-only + --dev /dev # private /dev + --proc /proc # procfs + --tmpfs /tmp # private /tmp + --bind "$sandbox_home" "${real_home}" # replace HOME with sandbox copy + --ro-bind "$topdir" "$topdir" # repo read-only + --bind "$review_dir" "$review_dir" # review output writable +) + +# Re-expose directories under real HOME that contain agent binaries +# and their libraries/runtimes (symlinks often point into lib dirs). +# These are mounted read-only on top of the sandbox HOME. +for d in \ + .local/bin .local/share/cursor-agent \ + .npm-global \ + .cargo/bin \ + .nvm .fnm .volta \ +; do + p="${real_home}/$d" + if [ -d "$p" ]; then + bwrap_args+=(--ro-bind "$p" "$p") # agent binary dir, read-only + fi +done + +exec bwrap "${bwrap_args[@]}" -- "$@"
diff --git a/misc/firejail-review-agent.sh b/misc/firejail-review-agent.sh new file mode 100755 index 0000000..aa8093f --- /dev/null +++ b/misc/firejail-review-agent.sh
@@ -0,0 +1,55 @@ +#!/bin/bash +# firejail-review-agent.sh -- firejail sandbox for b4 review agents +# +# The repository and real HOME are mounted read-only. HOME env is +# redirected to a temporary copy containing only agent auth config. +# .git/b4-review/ is the sole writable path in the repository. +# +# Requires: firejail +# +# Note: firejail cannot fully hide the real HOME from the agent +# (read-only != invisible). For stronger isolation, prefer +# bwrap-review-agent.sh which replaces HOME entirely. +# +# Usage -- set as your review-agent-command in git config: +# +# [b4] +# review-agent-command = misc/firejail-review-agent.sh cursor-agent --yolo +# review-agent-prompt-path = .git/agent-reviewer.md + +set -euo pipefail + +mydir="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=review-agent-sandbox-lib.sh +. "$mydir/review-agent-sandbox-lib.sh" + +if ! command -v firejail >/dev/null 2>&1; then + echo >&2 "firejail-review-agent: firejail not found, running agent without sandbox" + export HOME="$sandbox_home" + exec "$@" +fi + +# Mount layout: +# +# $real_home read-only real HOME (still visible but not writable; +# ~/.ssh, ~/.gnupg are readable -- use bwrap +# wrapper if this is a concern) +# $topdir read-only the git repository +# $review_dir read-write .git/b4-review/ -- review output +# /tmp read-write private tmpfs (empty, ephemeral) +# /dev read-write minimal private devtmpfs +# HOME env sandbox_home agent looks here for config by default +# +# Capabilities: all dropped. No new privileges, no root. +# +export HOME="$sandbox_home" +firejail --noprofile \ + --read-only="${real_home}" \ + --read-only="$topdir" \ + --read-write="$review_dir" \ + --private-tmp \ + --private-dev \ + --caps.drop=all \ + --nonewprivs \ + --noroot \ + -- "$@"
diff --git a/misc/review-agent-sandbox-lib.sh b/misc/review-agent-sandbox-lib.sh new file mode 100644 index 0000000..df588ff --- /dev/null +++ b/misc/review-agent-sandbox-lib.sh
@@ -0,0 +1,44 @@ +# review-agent-sandbox-lib.sh -- shared setup for sandbox wrappers +# +# Sourced by firejail-review-agent.sh and bwrap-review-agent.sh. +# Not executable on its own. +# +# After sourcing, the following variables are set: +# topdir -- repository top-level directory +# review_dir -- .git/b4-review/ (writable review output) +# real_home -- the user's real HOME +# sandbox_home -- temporary HOME with only agent auth config +# +# A trap is registered to clean up sandbox_home on exit. + +topdir="$(git rev-parse --show-toplevel)" +review_dir="$topdir/.git/b4-review" +mkdir -p "$review_dir" +real_home="${HOME}" + +# Create a temporary HOME containing only agent auth config. +# The agent process sees this as $HOME, so ~/.ssh, ~/.gnupg, etc. +# simply do not exist. +mkdir -p "$review_dir/.sandbox" +sandbox_home="$(mktemp -d --tmpdir="$review_dir/.sandbox")" +trap 'rm -rf "$sandbox_home"' EXIT INT TERM + +# Copy top-level config files (auth, settings) for known agents. +# Only files under 1 MB are copied -- large session logs and caches +# are skipped since the agent only needs credentials. +for d in \ + .config/cursor .config/Cursor .cursor \ + .config/claude .claude .local/share/claude .local/state/claude \ + .codex \ + .gemini .config/gemini \ +; do + src="${real_home}/$d" + if [ -d "$src" ]; then + mkdir -p "$sandbox_home/$d" + find "$src" -maxdepth 1 -type f -size -1024k \ + -exec cp -a {} "$sandbox_home/$d/" \; + elif [ -e "$src" ] || [ -L "$src" ]; then + mkdir -p "$(dirname "$sandbox_home/$d")" + cp -a "$src" "$sandbox_home/$d" + fi +done