Problem
The sandbox isolation contract is written three times. The three copies no longer agree.
config/sandcastle/orchestrate.mjs:307-384 defines 9 probes and runs them on every real
run. config/sandcastle/selftest.mjs:58-85 defines 11 probes and runs them only in
agentbox selftest. config/sandcastle/adversarial.mjs:106-111 holds a third list.
docs/sandcastle.md:756 states:
agentbox selftest proves each row from inside a running sandbox, and the same
probes run on every run and pipeline.
The probes are not the same.
Probes that no real run proves
Three probes exist only in selftest.mjs:
no credential file from the host home — it tests ~/.claude/.credentials.json and
~/.codex/auth.json. Both are rows of the What never enters a sandbox table in
docs/sandcastle.md:746-747.
the sandbox is not privileged — it reads capsh --print for cap_sys_admin.
the worktree is mounted and is a git worktree.
Probes that drifted
| Probe |
orchestrate.mjs |
selftest.mjs |
| private key |
-name 'id_*' ! -name '*.pub' |
id_rsa*, id_ed25519*, id_ecdsa*, id_dsa* |
| Podman socket |
test ! -S |
test ! -e |
The two socket tests are not equivalent. test ! -S passes when a podman.sock exists as
a regular file.
The podman({imageName, mounts, selinuxLabel, userns, containerUid, containerGid})
provider block is also written three times: orchestrate.mjs:286-298,
selftest.mjs:114-121 and adversarial.mjs:95-102.
verify/80-sandcastle.sh cannot see the drift. Check D6 searches $ORCH for the string
is absent from the sandbox.
A second integrity contract with the same shape
bin/agentbox:774-836 (repo_snapshot, diff_snapshot) and
config/sandcastle/clone-integrity.mjs:67-116 (snapshotIntegrity, diffIntegrity) both
answer one question: did anything about this Git repository change across the run? They
disagree on how.
| Property |
bin/agentbox |
clone-integrity.mjs |
| config |
sha256sum of the whole file |
config --local --list, per entry |
| hooks |
mode, size and SHA per file, .sample included |
mode and size, .sample filtered |
| working tree |
status --porcelain |
not covered |
The bash side hashes the whole config file, so a violation reports two opaque hashes. The
JavaScript side names the exact key, for example git config added: alias.pwn. The weaker
report guards the real repository. diffIntegrity has a 317-line probe.
diff_snapshot has none.
Do not merge the two implementations. The host half must run when the machine has no Node,
which verify/80-sandcastle.sh:53-58 already depends on.
Goal
State the sandbox isolation contract once, and prove every row of the documented table on
every run.
Proposed direction
- Add
config/sandcastle/isolation.mjs, beside clone-integrity.mjs and budget.mjs.
Export isolationProbes(cfg), which returns the probe records, and
evaluateProbeOutput(raw), which decodes clean, LEAK, MISSING and STALE_IMAGE.
- Stage it the way
stage_script already stages its two siblings (bin/agentbox:973).
- Let
orchestrate.mjs, selftest.mjs and adversarial.mjs declare only the probes they
add. orchestrate.mjs adds the forbiddenPaths probes; adversarial.mjs adds its
negative cases.
- Resolve each drifted probe deliberately. Record which test is correct, and use it in
both places.
- Extract the shared
podman({...}) provider block the same way.
- Give
repo_snapshot/diff_snapshot and snapshotIntegrity/diffIntegrity one property
set and one violation grammar, then drive both against one fixture corpus.
clone-integrity.mjs:10-12 already states the reason this shape is right:
It lives in its own file so that the comparison can be exercised directly, with no
container and no model credential.
Acceptance criteria
Problem
The sandbox isolation contract is written three times. The three copies no longer agree.
config/sandcastle/orchestrate.mjs:307-384defines 9 probes and runs them on every realrun.
config/sandcastle/selftest.mjs:58-85defines 11 probes and runs them only inagentbox selftest.config/sandcastle/adversarial.mjs:106-111holds a third list.docs/sandcastle.md:756states:The probes are not the same.
Probes that no real run proves
Three probes exist only in
selftest.mjs:no credential file from the host home— it tests~/.claude/.credentials.jsonand~/.codex/auth.json. Both are rows of the What never enters a sandbox table indocs/sandcastle.md:746-747.the sandbox is not privileged— it readscapsh --printforcap_sys_admin.the worktree is mounted and is a git worktree.Probes that drifted
orchestrate.mjsselftest.mjs-name 'id_*' ! -name '*.pub'id_rsa*,id_ed25519*,id_ecdsa*,id_dsa*test ! -Stest ! -eThe two socket tests are not equivalent.
test ! -Spasses when apodman.sockexists asa regular file.
The
podman({imageName, mounts, selinuxLabel, userns, containerUid, containerGid})provider block is also written three times:
orchestrate.mjs:286-298,selftest.mjs:114-121andadversarial.mjs:95-102.verify/80-sandcastle.shcannot see the drift. Check D6 searches$ORCHfor the stringis absent from the sandbox.A second integrity contract with the same shape
bin/agentbox:774-836(repo_snapshot,diff_snapshot) andconfig/sandcastle/clone-integrity.mjs:67-116(snapshotIntegrity,diffIntegrity) bothanswer one question: did anything about this Git repository change across the run? They
disagree on how.
bin/agentboxclone-integrity.mjssha256sumof the whole fileconfig --local --list, per entry.sampleincluded.samplefilteredstatus --porcelainThe bash side hashes the whole config file, so a violation reports two opaque hashes. The
JavaScript side names the exact key, for example
git config added: alias.pwn. The weakerreport guards the real repository.
diffIntegrityhas a 317-line probe.diff_snapshothas none.Do not merge the two implementations. The host half must run when the machine has no Node,
which
verify/80-sandcastle.sh:53-58already depends on.Goal
State the sandbox isolation contract once, and prove every row of the documented table on
every run.
Proposed direction
config/sandcastle/isolation.mjs, besideclone-integrity.mjsandbudget.mjs.Export
isolationProbes(cfg), which returns the probe records, andevaluateProbeOutput(raw), which decodesclean,LEAK,MISSINGandSTALE_IMAGE.stage_scriptalready stages its two siblings (bin/agentbox:973).orchestrate.mjs,selftest.mjsandadversarial.mjsdeclare only the probes theyadd.
orchestrate.mjsadds theforbiddenPathsprobes;adversarial.mjsadds itsnegative cases.
both places.
podman({...})provider block the same way.repo_snapshot/diff_snapshotandsnapshotIntegrity/diffIntegrityone propertyset and one violation grammar, then drive both against one fixture corpus.
clone-integrity.mjs:10-12already states the reason this shape is right:Acceptance criteria
docs/sandcastle.mdhas a probe that runs on a normalagentbox run.podman({...})provider block has one definition.repo_snapshot/diff_snapshotreports the Git config per entry, not as one hash.docs/sandcastle.md:756is true, or it states the exact difference../verify.shremains green.agentbox selftest --adversarialremains green on a live machine.