With the flag set, ensure_container omits the /mission bind, the checkout
is pushed into the container at phase launch, and the agent's work is
pulled back before capture.
The simplification that makes this small: sync_out unpacks over the SAME
host path the checkout came from. The host directory stays a server-owned
staging area with exactly one writer, and capture_phase_diff_at needs no
change at all — it still finds a normal checkout exactly where it always
has. Delivery, gating, commit and push are untouched.
Two failures are deliberately loud rather than silent:
- copy-IN failure fails the phase launch. Continuing would start a phase
against an empty directory, and the agent would cheerfully report having
done work on a repo that was not there.
- copy-OUT failure SKIPS capture. Capturing anyway would diff a stale host
tree and record "no changes" for work that exists — success reported for
nothing, which is the exact failure mode this codebase keeps paying for.
Opt-in: the bind path is what production has run since the beginning, and
the test asserts a near-miss value leaves it there rather than silently
switching every mission.
414 tests, clippy clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
The first half of removing the shared bind mount. Not wired yet — this
adds the mechanism and its tests.
One cause, four fixes so far: .git/objects permission denied
(core.sharedRepository), the capture base being overwritten each phase,
COMMIT_EDITMSG root-owned, and reset --hard deleting a prior phase's work
(.git/clawmates-in-use). core.sharedRepository was never a general
solution — it covers objects and refs, and every OTHER file git touches
is a fresh opportunity. Copy-in/copy-out removes the cause instead: the
agent owns its filesystem with no second writer.
Measured before building, because the plan named copy cost as the open
risk: a real 65 MB checkout of this repo copies in 0.23s and out 0.18s on
gw-04. Not a risk at this size; re-measure an order of magnitude larger.
No compression — the payload crosses a local socket, so gzip would spend
CPU to save nothing.
Two safety properties, both tested:
- The archive comes back from a container the agent controls as ROOT, so
it is untrusted input. A `../ESCAPED` entry must not write outside the
destination. The test writes the tar header bytes by hand because the
tar crate refuses to BUILD such an entry through its safe API — which
is reassuring, but means the hostile case has to be constructed the way
an attacker would.
- Symlinks are packed as links, never dereferenced. Following them on
copy-IN would smuggle host files into the container; the test plants a
host secret behind a symlink and asserts its contents never appear in
the archive.
Ownership is deliberately not preserved on unpack: the archive's uids are
the container's root, and re-applying them on the host would recreate the
exact uid split this exists to remove.
413 tests, clippy clean.
Co-Authored-By: Claude Opus 5 <[email protected]>