Commit Graph
3 Commits
Author SHA1 Message Date
Omar SobhandClaude Opus 5 dcd9514622 fix(exec): mission work runs as uid 65532, so it stops creating debris it cannot delete
`CreateExecOptions` never set `user`. Not a wrong value — an ABSENT one: the
daemon defaults to root, and twelve callers inherited that without any of them
choosing it. That single omission is the origin of four separate patches —
root-owned `target/` directories inside a checkout owned by 65532, `root_copy`
existing at all, and a cleanup that had to re-enter the container as root to
undo its own mess.

The rule is positional and lives in ONE place: an exec whose workdir is inside
`missions_root()` runs as 65532; anything else (preflight probes, image checks)
keeps the daemon default so unrelated call sites cannot break. Twelve callers
each remembering to pass a uid is twelve chances to forget, and the one that
forgets leaves debris the other eleven cannot remove.

Non-root needs an environment the image does not provide. Measured in the
deployed image: uid 65532's HOME (/zeroclaw-data) and /usr/local/cargo are both
root-owned and unwritable, so this would otherwise break every cargo call — the
benchmark runner, the judge's sandbox, the delivery test gate — far more quietly
than the leak it fixes. The missions root IS bind-mounted and writable by 65532,
so HOME/CARGO_HOME move there and the cargo cache is shared across missions
rather than re-fetched per mission. Verified on gw-04: a clean `cargo build` as
65532 with those three variables produces output owned entirely by 65532.

Root remains reachable only through `exec_as_root`, whose name says so, and
which exists solely to clear debris earlier root execs left. `runtime_preflight`
now probes the whole policy at boot, so an image that moves or tightens that
mount fails loudly instead of failing every cargo call for a reason no error
message would connect to a uid. evaluator_tools' inlined fourth copy of the
purge is replaced by `root_copy::purge`.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-09 10:47:59 -07:00
Omar Sobh 5c5f1ced33 refactor: the judge's sandbox joins the other three copy sites on root_copy
Four places copy a mission checkout so a ROOT command can run against it without
touching the live tree: the judge, the benchmark runner, the on_green_tests gate,
and — until now — the judge again, with its own implementation predating the
shared one.

`evaluator_tools::Sandbox::for_checkout` now builds through `root_copy::RootCopy`.
Same packer, same exclusion list, same reasoning in one place.

It needs the copy to OUTLIVE the handle, because the judge has not run when
`for_checkout` returns and a firing `Drop` would delete the tree out from under
it. That is `into_workdir`, a method rather than a `mem::forget` at the call
site: the transfer of cleanup responsibility is then visible in the type instead
of implied by a leak. `Sandbox::purge` remains what actually clears it, since
only a container running as root can remove the root-owned `target/`.

What did NOT move: `pack_dir` in the microVM inject/collect path. That marshals a
tree to and from a guest over vsock — a transport, not a host-side copy — and
folding it in would merge two things that only look alike.

249 lib tests.
2026-08-08 06:12:34 -07:00
Omar Sobh a8b8efba6a fix(delivery): the on_green_tests gate ran the suite in the live checkout
Fourth instance of the same defect, and the last of the three commands that run
as root against a mission tree.

`verify_tests` execs the project's test command with `workdir = repo` — the live
checkout — inside a container running as ROOT. `cargo test` writes `target/`, so
the checkout ends up owned by two uids and the next phase's cargo hits
permission-denied. The harness reported `uids=0,65532` the first time this gate
ever ran end to end.

It survived because it had never run. Every one of the ten harness fixtures used
`commit_policy: "always"`; `on_green_tests` and `on_reviewer_approval` were
parsed, implemented, and never exercised — and `Gate`'s own doc already records
that three recipes carried this policy while it "did precisely nothing" for want
of a reader. A policy that is never exercised is indistinguishable from one that
is ignored.

Consolidated rather than fixed a third time. `root_copy` now owns the pattern —
copy through `mission_fs::pack_dir` into a SIBLING of the mission dir, run there,
and purge FROM INSIDE THE CONTAINER, because the copy's `target/` is root-owned
and the server (uid 65532) cannot delete it. `benchmark_runner` moved onto it;
`evaluator_tools::Sandbox` keeps its own copy logic for now (it carries an
allow-list and a judge-facing API, so folding it in is a larger change than this
moment warrants — noted, not done).

The gate fails CLOSED if the copy cannot be made: an unverifiable suite must not
license a push.

Also adds the `refactor` scenario, which is what found this. I had written it off
as "structurally identical to four existing scenarios" — wrong: it is the only
recipe carrying `on_green_tests`, and that made it the only one testing this
code path at all.

245 lib tests, 20 test binaries.
2026-08-07 17:48:29 -07:00