feat(jail): fs-jail mode — confine worker reads to a system+toolchain allowlist (WORKER_FS_JAIL) - #79
Open
drewstone wants to merge 1 commit into
Open
feat(jail): fs-jail mode — confine worker reads to a system+toolchain allowlist (WORKER_FS_JAIL)#79drewstone wants to merge 1 commit into
drewstone wants to merge 1 commit into
Conversation
… allowlist (WORKER_FS_JAIL)
The host write-jail mounts the whole host root read-only (`--ro-bind / /`),
so a spawned coder CLI's bash can read the entire filesystem — including the
benchmark repo's task definitions and grader answer keys. A coder was observed
grepping /home/drew/code/blueprint-agent/.../infra/api-ghost-admin.ts to read
the hidden task + grader, i.e. cheating. Only WRITES were jailed; reads were not.
Add an `fs-jail` mode (superset of write-jail) that, on Linux/bwrap, drops the
whole-root read bind and instead binds ONLY:
- a minimal read-only system allowlist (/usr + /bin,/sbin,/lib* via ro-bind-try,
/etc for resolv/ssl/nss, /run/systemd/resolve for DNS, /opt), /proc, /dev;
- a FRESH empty tmpfs at /tmp (so host /tmp twins + other runs are invisible);
- the language + CLI toolchain, auto-derived at wrap time (node install prefix,
the CLI's on-PATH dir + realpath install root) with a fail-closed guard that
refuses /, /home, $HOME, or any ANCESTOR of the workspace; operator-extensible
via BRIDGE_JAIL_RO_PATHS;
- the workspace, bound READ-WRITE, so a coding agent still builds its solution.
Backends whose MCP/runtime config is materialized under the host /tmp
(opencode, kimi) re-expose those dirs read-only via registerJailReadable so the
tmpfs does not hide them. Network is untouched (--share-net) so the localhost
twin and real APIs stay reachable.
Gated + fail-closed: default off; opt in with WORKER_FS_JAIL=1 (shorthand) or
BRIDGE_JAIL_MODE=fs-jail / per-request execution.jail.mode=fs-jail. The env
floor is a MAX — a request can raise confinement, never lower it. When a jail is
requested but bwrap can't run here, the server still fails closed (unchanged).
Proof (scripts/verify-fs-jail.ts, real bwrap spawn on this host):
BLOCK cat host secret / ls /home/drew/code / cat the real ghost-admin file
ALLOW write in workspace, node -e, python3 -c, curl the localhost twin
Composition tests assert the argv shape + floor semantics; full suite green
(41 jail tests, 322 total).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The hole
The host write-jail (#64) mounts the whole host root read-only —
--ro-bind / /insrc/jail/linux-bwrap.ts. It confines a spawned coder CLI's writes to a scratch root but leaves reads wide open. So a coder running under the host executor can read the entire filesystem, including the benchmark repo that holds the task definitions and grader answer keys.Observed in a live run — a coder escaped its clean temp workspace and read the hidden task + grader directly:
The workspace is a clean temp dir, but only writes were jailed; reads were not. (The heavy VB pools already run
OPENCODE_EXECUTOR=dockerand are fs-isolated; this closes the host-executor path, which was unconfined.)The fix — an
fs-jailmode (bwrap allowlist)fs-jailis a superset ofwrite-jail: same write confinement, plus read confinement. On Linux/bwrap it drops--ro-bind / /and binds only:/usr(+/bin /sbin /lib*via--ro-bind-try, which follow the merged-usr symlinks),/etc(resolv/ssl/nss),/run/systemd/resolve(DNS),/opt; plus--proc /proc,--dev /dev;--tmpfs /tmp— the host/tmp(sibling-run twins, other runs' scratch) is invisible;/,/home,$HOME, or any ancestor of the workspace (so it can never re-open the home tree or a/tmpsibling). Operator-extensible viaBRIDGE_JAIL_RO_PATHS;Backends that materialize MCP/runtime config under the host
/tmp(opencode, kimi) re-expose those specific dirs read-only through a newregisterJailReadablehelper, so the fresh tmpfs doesn't hide them. Network is untouched (--share-net, no--unshare-net) so the localhost twin and real APIs stay reachable. The tangle-router inference call is the bridge's own network, not the worker's, and is unaffected.Env flag + rollout (fail-closed, default off)
Default is off — live runs are untouched until an operator opts in. Turn it on with any of:
WORKER_FS_JAIL=1— shorthand for the fs-jail floor;BRIDGE_JAIL_MODE=fs-jail— same, via the existing jail-mode env;execution.jail.mode: "fs-jail".The env floor is a MAX: a request can raise confinement (
write-jail→fs-jail) but never lower it. If a jail is requested and bwrap can't run here, the server still fails closed at startup (unchanged;BRIDGE_JAIL_FALLBACK=warnto override).Proof —
scripts/verify-fs-jail.ts(real bwrap spawn, this host)Composition tests (
tests/jail.test.ts) assert the argv shape (no whole-root read bind, tmpfs/tmp, workspace read-write, floor MAX semantics). Full suite green: 41 jail tests, 322 total (pnpm test);tsc --noEmitclean. bwrap 0.9.0 is installed on the host.Operator: restart the bridge with the jail ON (after merge)
fs-jailneeds unprivileged user namespaces. Enable once (host is modern Ubuntu):Then restart the bridge with the flag set, e.g.:
On boot the log prints
[cli-bridge] jail default: fs-jail root=<cwd>/.agent-home. To confirm the jail holds on the target host:pnpm verify:fs-jail.Note: do NOT restart the currently-running bridges (
:3355,:3390) as part of merging — other live runs depend on them. This PR is build-only; the operator restarts on their own schedule.