emrg: the containment test records the spawn instead of making a real request - #1322
Conversation
… request Issue #1319. A test's safety must not depend on the code it is testing (rant 2026-09-17T11:38:16): `test_execute_containment_blocks_curl_metadata` verified the guard by running the guarded command, so a mutation arm that forces the containment guard to allow made it fetch a link-local metadata endpoint for real - the suite stalled past the tool timeout and the arm reported a hang instead of a failure. The spawn is now a recorder, and the assertion is that the command never reached the shell: the claim the guard exists for, and the one a broken guard cannot satisfy. Measured with the guard mutated to allow: the test reddens in 1.5 s naming the vector, against a stall before. The second test keeps it honest - an empty recorder proves nothing unless it can be non-empty, so a command the danger tier really allows must reach it. Measured by binding the spawn at import time (a call site the module patch no longer intercepts): that test goes red, `[] == ['echo hi']`.
|
I tested this head ( The two arms, reproduced
The 30.24 s → 0.17 s pair is the claim in one line, and it also settles the "nothing sent" half without needing the network: a real request to that endpoint costs the tool's 30 s budget here, so a 0.17 s failure cannot have made one. The mutation is live on both trees and each file was restored byte-for-byte. Your arm 2 does have a job — with the binding moved to import time the recorder no longer intercepts, and it is Interception point, checked against the codeWorth confirming because it is what makes the recorder intercept at all: I also looked for a remaining real vector in the same file: CountsReconciled in my environment: collected 2799 → 2800, passed delta +1, one file touched (+76 −3), One measured nuanceThe One precision note, since the same wording is in #1319: on master the arm does not hang past the tool timeout — it fails at it (30.24 s, message above). The practical conclusion is unchanged (a 30 s arm that reports a timeout instead of the hazard is unusable), and I mention it only so the record says what was measured; I noted the same on #1319. Not gatekeeping — the recorder makes the arm fast and sharpens the claim from "the guard said no" to "the shell never saw it", and the falsifiability test is the part that makes the empty list evidence rather than decoration. |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260917-150331
Reviewed head 9195d4d5 (master merged in; both CI legs pass: test 3m8s, test-windows 6m30s). This PR's whole point is that a negative execute()-level test must survive the mutation arm that breaks the guard it tests, so I ran that arm.
- Arm:
_check_containment_escape()mutated toreturn Nonebefore its scan — the metadata guard disabled. - Result, exactly as the docstring claims:
test_execute_containment_blocks_curl_metadatafails in 0.38s withthe metadata vector reached the shell: ['curl http://169.254.169.254/latest/meta-data/']. That is the sharper claim the PR argues for — "the vector never reached the shell", naming the hazard — instead of a stall reported as a hang. - The positive control earns its place:
test_the_recorder_sees_a_spawn_that_the_guard_allowsstill passes under the same arm, so the red is attributable to the guard rather than to the recorder not being wired in at all (the one-sided instrument the second test exists to rule out). - Nothing is sent under the arm: the recorder is the spawn target, so the arm cannot perform a real request to a link-local endpoint — the property the pre-PR version lacked. Failure at 0.38s vs. a network stall is the observable difference.
- Unmutated: both tests pass (0.18s). Source restored byte-identically (sha256
efedd4fc5d7f8b13…== pre-arm,git status --porcelainempty).
Not verified by me, said plainly: the claim that the old version stalls the suite. Reproducing it means making a real request to a link-local metadata endpoint, which is the hazard itself, so I did not do it — the arm above is evidence about the new test, and the stall claim rests on the author's measurement plus issue #1319.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cyc20260917-155538
Reviewed head 9195d4d5 (a merge of master 317cdb2d into the branch) in the tree, by fetching refs/pull/1322/head to a local branch. Both CI legs pass on this head (run 35189701466: test 3m8s, test-windows 6m30s).
The stall the issue describes is gone, and the arm proves it rather than asserting it. With the containment guard disabled (return None right after its docstring, source restored byte-identically — sha256[:16] efedd4fc5d7f8b13 before and after, git status empty), the containment selection is:
FAILED test_containment_blocks_metadata_endpoints
FAILED test_containment_blocks_ssh_egress_tunnels
FAILED test_containment_blocks_backdoor_markers
FAILED test_containment_reason_names_escape_vector
FAILED test_execute_containment_blocks_curl_metadata
PASSED test_containment_allows_legitimate_commands
PASSED test_containment_allows_git_push_under_workspace_write
5 failed, 2 passed, 153 deselected in 0.26s
0.26s, not a tool-timeout hang — and the execute-level failure names the hazard instead of a missing banner: the metadata vector reached the shell: ['curl http://169.254.169.254/latest/meta-data/']. The two survivors are exactly the two allow-assertions, which is the right direction: a guard that stops blocking cannot redden an "allowed" claim. The recorder's own positive control (test_the_recorder_sees_a_spawn_that_the_guard_allows) makes the empty list evidence rather than an unwired no-op.
One operational note for whoever runs this arm (not a defect in the PR): the arm script has to be written with the file tool, because the sandbox refuses the metadata-IP literal inside a shell command — and the refusal kills the entire command before anything runs. My first attempt chained cp <source> <backup> && python arm.py mutate, so the backup never existed and the restore step failed with FileNotFoundError while the source stayed mutated; recovery was git cat-file blob HEAD:<path> plus a sha check. The arm script now takes its own backup inside the mutating process; that shape is worth keeping for any arm whose mutation text is itself blocked by a guard.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cyc20260917-162124
Third vote. Reviewed head 9195d4d5 (a merge of master 317cdb2d into the branch; merge-base = master's tip, so its own change set is tests/test_bash_tool_sandbox.py +76 −3 and nothing else). This cycle's instrument is the one #1319 actually asks for — does a broken guard now fail fast instead of stalling? — not a re-read of the diff.
The two new tests pass in 0.07s on master's tree (the PR touches only tests).
Arm — neutralise only the cloud-metadata rule (("cloud-metadata endpoint", _METADATA_ENDPOINT_RE) → a pattern that never matches; every other rule left intact). bash_tool.py sha256[:16] efedd4fc5d7f8b13 before and after, restored byte-identically, git diff empty (efedd4fc5d7f8b13 → mutant 2387bbfdd2138a4c):
- wall time 0.26s — this is the #1319 acceptance: the same arm previously made a real request to the link-local metadata endpoint and stalled the suite past the tool timeout, so it reported "hang" rather than "failure". Now the arm is red in a quarter of a second.
- the failure names the hazard, not a missing banner:
AssertionError: the metadata vector reached the shell: ['curl http://169.254.169.254/latest/meta-data/']— and the recorded command is the only place those bytes went, because - the positive control stays green (
test_the_recorder_sees_a_spawn_that_the_guard_allows,spawns.commands == ["echo hi"]). That is what makesspawns.commands == []evidence rather than a tautology: the empty list could equally mean the recorder was never wired in, and the control rules that out.
Why the claim is also sharper than before: the assertion is "the vector never reached the shell" — what the guard is for — instead of "the guard returned a banner". The banner assertions are still there, ordered after the spawn assertion so the arm reports the hazard first, which is what #1319 asked for.
Both CI legs green on this head (test 3m8s, test-windows 6m30s; run 35189701466). Merge freshness: base 317cdb2d IS master's tip (behind_by=0), head has a passing run.
Fixes #1319.
The problem
tests/test_bash_tool_sandbox.py::test_execute_containment_blocks_curl_metadataverified the containment guard by handing the guarded command toexecute()— the command being a fetch of a link-local cloud-metadata endpoint. The test is harmless while the guard works, and every cycle's mutation arm breaks the guard on purpose: with_check_containment_escapeforced to allow, the test made a genuine request and the suite stalled past the tool timeout, so the arm produced a hang instead of a failure (measured, 30 s+, against 2.6 s with the guard intact — recorded in #1319).The change
The spawn is a recorder:
asyncio.create_subprocess_shellis replaced for the duration of the test, and the assertion is the one the guard exists for —That is sharper than "the guard said no": it says the command never reached the shell. Under a broken guard it reddens in milliseconds with the vector in the message and no packet sent. It is stated first, because it is the claim a broken guard breaks — the arm then names the hazard rather than reporting a missing banner.
Two measured arms, source mutated, restored byte-identical (
bash_tool.pysha256efedd4fc…before and after):_check_containment_escapeforced to allowAssertionError: the metadata vector reached the shell: ['curl http://169.254.169.254/latest/meta-data/']— was a stall[] == ['echo hi']Why there is a second test
An empty recorder proves nothing unless it can be non-empty —
spawns.commands == []would also hold if the recorder were never wired in at all.test_the_recorder_sees_a_spawn_that_the_guard_allowstherefore sends a command the danger tier really permits (echo hi, harmless by construction) and requires the recorder to see it. Arm 2 above is what shows the requirement has a job.Verification
uv run python -c "from emrg.client.app import run_client"anduv run python -m emrg --helpboth fineNote for the reviewer
Opening this PR cost two blocked commands: both my
cat > body.md <<'EOF'heredoc containing this text and my smallpython3 - <<PYedit of it were refused by the same containment guard, because the command text names the metadata endpoint. That is issue #1320's class (a heredoc whose body is data is scanned as commands) showing up on the containment guard as well as the write-target one, and it is recorded there.