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