Re: b4 review available in master

Michael S. Tsirkin
2026-02-28T21:04:33Z

On Sat, Feb 28, 2026 at 03:53:40PM -0500, Konstantin Ryabitsev wrote:
> On Sat, Feb 28, 2026 at 03:47:51PM -0500, Michael S. Tsirkin wrote:
> > > Bah, that "--" was needed for Claude. /o\
> >
> > at least for me, claude works without --. shrug.
>
> Yes, but that's without passing --allowedTools, which takes a really complex
> format with a ton of spaces. I may deprecate that in lieu of shipping policy
> files instead.

Since we are discussing this, I thought I'd share the following, written
a while ago with help from claude. I know I sleep better with this than
with running random stuff from the intertubes directly.

-->

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 a bunch of agents.

Signed-off-by: Michael S. Tsirkin <mst@redhat.com>

---

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