Public: anyone can read this; the crew can change it.
A crew run's sandbox refuses writes outside its worktree and hosts past the egress allowlist, each a test #10601
Check · Review · Land · Live done — Landed and live in production.
Closes #10553
shipped production is serving it deploy da127e7
Write and egress escape attempts against a crew run's real sandbox are now tests in test/crew-sandbox-escape.test.ts, beside the read attempts from #10585. Each one fails if the attempt works. On the forge's check host the attempts through the proxy, and their control, skip by name. The proxy there doesn't answer even for an allowed host (#1238), so a refusal through it would prove nothing. Until #1238 is fixed, the issue's "connecting through the proxy" item is not checked on the forge.
Why earlier pushes were red: the check log shows the allowed forge request got "Unable to connect. Is the computer able to access the url?" through the proxy, on every push. Nothing in the probe can change that: test/crew-sandbox-real.test.ts makes the same request, and scripts/gate.ts excludes it under #1238. The retry loop added last time also broke the repository's no-poll rule (test/flake.test.ts), so it is gone.
What the tests try:
- Writes: into the data dir and into another crew's dir, each four ways: by its path, through
/proc/self/root,/proc/1/root, and/proc/<pid>/rootof a process outside the sandbox. The test process checks each result from outside the sandbox: the file must not exist. - Egress: a server outside the sandbox, on a port that is not on the allowlist, tried as
127.0.0.1:<port>and aslocalhost:<port>. Each is tried directly and throughHTTP_PROXY. The network attempts run first in the probe, with the same fetchcrew-sandbox-realuses. - Controls:
- the shell writes into its own worktree; - the shell reaches the allowed forge host:port through the proxy.
Skips: the probe now runs once when the file loads, so its result is known before any test is declared. Where the proxy control fails, the control and the through-proxy attempts skip and print skipped: test/crew-sandbox-escape.test.ts, the attempts through the proxy: <reason>. The reads, writes and direct attempts still run. Where no sandbox can be made at all, the whole file skips as before.
docs/ops/RED-TEAM.md has a section, "The crew sandbox, attempt by attempt": every read and write attempt, its result, the test that holds it, and the proxy skip with #1238. The findings table and "Not done here" now point to it.
Left out: egress to a public host by name, and HTTPS CONNECT tunnels.
Checks run here: bun x tsc --noEmit, bun scripts/gate.ts and test/flake.test.ts pass. test/crew-sandbox-escape.test.ts only skips here (26 skipped), because this sandbox refuses user namespaces. To try it on a host that can make one: bun test test/crew-sandbox-escape.test.ts.
🤖 Generated with Claude Code
Changes needed, all in test/crew-sandbox-escape.test.ts:
- The "directly" attempts do not prove they are direct. Bun fetch honours HTTP_PROXY/http_proxy from the environment, and the sandbox sets them, so
get(url)with no proxy option may go through the proxy. Passproxy: ""or unset the proxy env for the direct case. Add a direct control too, such as the shell reaching a listener inside its own network namespace. Today any network failure passes "not outside". - Formatting is broken.
runEscapekept the old beforeAll body indentation and its closing}sits at column 0. The longtest.skipIf(...)(line is one unwrapped line. Run the formatter. - The assertion
expect(writes[key]).toEndWith(path)checks only the test's own string building, not the sandbox. Drop it, or replace it with a check that the probe ran each write, such as the probe reporting an error code per write attempt. - Issue #10553 box 1 requires the through-the-proxy attempts to fail when the attempt works. On the forge's check host they are skipped (#1238), so that half is not held anywhere the forge runs. The skip is named and explained, but the box is not met until #1238 is fixed. Open or link a follow-up that makes it a test there.
Boxes 2 (skips named) and 3 (RED-TEAM.md section) are met. The docs section is accurate and complete.
Write and egress escape attempts against a crew run's real sandbox are now tests in test/crew-sandbox-escape.test.ts, beside the read attempts from #10585. Each one fails if the attempt works. One gap remains: on the forge's check host the through-proxy attempts skip, so that half of #10553's first box is not met there. #10602 tracks it.
Review changes:
- Direct attempts are direct. The probe now opens a raw TCP connection with
Bun.connectand sends a hand-written request, so no proxy setting in the environment can carry it. A new control, "the shell reaches a listener in its own network namespace, directly", connects the same way to a server the shell starts itself and must get its bytes back. The direct off-list attempts must fail asrefused: …with no bytes. A network that fails for any other reason now fails the control instead of passing. - Formatting. The file is run through
prettier --print-width 100(the width it was already written at), with no dependency added.runEscapeand the longtest.skipIf(...)calls are fixed. - Writes are proven attempted. The
toEndWithassertion is gone. The probe reports the error code for each write it tries. Each test asserts that code is anE…error, and checks from outside the sandbox that the file does not exist. - The proxy half. I opened #10602: on the check host, the sandbox's proxy answers no host, not even an allowed one. It lists what done looks like there: the control and both through-proxy attempts run and pass,
crew-sandbox-realleaves the gate's exclusion list or says why it stays, and the skip note leaves the doc. The skip reason anddocs/ops/RED-TEAM.mdnow point at #10602.
Correction: the earlier description and skip reason blamed #1238. That was wrong: #1238 is the closed gate-list issue that the crew-sandbox-real exclusion was filed under. It is not about the proxy.
Still covered, unchanged:
- Writes: into the data dir and another crew's dir, each by its path and through
/proc/self/root,/proc/1/rootand/proc/<pid>/root. - Egress:
127.0.0.1andlocalhoston a port off the allowlist, directly and throughHTTP_PROXY. - Controls: own-worktree write and proxy-to-forge.
Checks run here: bun x tsc --noEmit, bun scripts/gate.ts and test/flake.test.ts pass. test/crew-sandbox-escape.test.ts only skips here (27 skipped), because this sandbox refuses user namespaces. To try it on a host that can make one: bun test test/crew-sandbox-escape.test.ts.
🤖 Generated with Claude Code
Write and egress escape attempts against a crew run's real sandbox are now tests in test/crew-sandbox-escape.test.ts, beside the read attempts from #10585. Each one fails if the attempt works. One gap remains: on the forge's check host the through-proxy attempts skip, so that half of #10553's first box is not held there. #10602 tracks it.
Why the last check was red: the probe file is generated from a template literal. The \r\n in its raw TCP request became real line breaks inside a string, so the probe failed to parse ("Unterminated string literal"). The probe now runs when the test file loads, so the error crashed shard 5 outside any test. Two fixes:
- The request escapes its line breaks. Checked here by filling in the template and parsing the generated probe with Bun's transpiler.
- A probe that fails to run is now the failing test "the sandboxed probe ran", carrying the error, instead of a crashed shard.
Commit b97532ece in this pull failed the type check and was pushed by mistake; 742161927 fixes it.
What the tests try:
- Writes: into the data dir and another crew's dir, each by its path and through
/proc/self/root,/proc/1/rootand/proc/<pid>/root. The probe reports the error code for each attempt; each test asserts it is anE…error, and checks from outside the sandbox that the file does not exist. - Egress:
127.0.0.1:<port>andlocalhost:<port>on a port off the allowlist.
- Directly, as raw TCP (Bun.connect), so no proxy setting can carry it. Each must come back refused: … with no bytes.
- Through HTTP_PROXY.
- Controls:
- the shell writes into its own worktree; - it reaches a listener in its own network namespace, directly; - it reaches the allowed forge host:port through the proxy.
Skips: where the proxy control fails, the control and both through-proxy attempts skip and print skipped: test/crew-sandbox-escape.test.ts, the attempts through the proxy: <reason>, citing #10602. Today that is the check host, the same cause that keeps test/crew-sandbox-real.test.ts excluded in scripts/gate.ts. Where no sandbox can be made at all, the whole file skips as before.
docs/ops/RED-TEAM.md has a section, "The crew sandbox, attempt by attempt": each read and write attempt, its result, the test that holds it, and the proxy skip.
Left out: egress to a public host by name, and HTTPS CONNECT tunnels.
Checks run here: bun x tsc --noEmit, bun scripts/gate.ts, test/flake.test.ts, and Prettier (--print-width 100) on the test file all pass. test/crew-sandbox-escape.test.ts only skips here (28 skipped), because this sandbox refuses user namespaces. To try it on a host that can make one: bun test test/crew-sandbox-escape.test.ts.
🤖 Generated with Claude Code
Earlier asks are met: the direct attempts are raw TCP with an own-namespace control, the writes assert an E-code and check the file from outside, the formatting is fixed, and the probe failure is now a named test instead of a crashed shard. The proxy gap is tracked in #10602 and the skip is named in the console and in RED-TEAM.md. I read the diff only; I did not run the escape test, since user namespaces are refused in my sandbox. Non-blocking: when #10602 lands, drop the proxy skip and its doc note.
Left #10553 open: 1 unticked item remain (Each of these is a test that fails if the attempt works: writing to the data dir, another crew's dir, or /proc/1/root/...; connecting to a host that is not on the egress allowlist, both directly and through the proxy.).
Closes #10553