fix(missions): stop three launch failures from passing as success

A verification run against the deployed stack found a chain mission whose
phase 0 reported `completed` with zero files, no commit error and no push
error — indistinguishable from a phase that correctly had nothing to do.
Three separate defects had to line up, each of them the same shape: a
failure sharing its representation with a legitimate negative result.

1. `pin_agent_workspaces` embedded the whole config in one `sh -c` argv.
   That works until the file grows — config gains a block per provisioned
   claw — then fails with `argument list too long`. Now written through
   the tar upload API, which has no argv limit, so the failure mode is
   gone rather than merely further away.

2. A failed pin was logged "(continuing)". Without the pin, agents write
   to their sandboxes and the committer finds nothing in /mission/repo —
   the mission cannot deliver, so the launch now fails where someone is
   still looking. The restart that applies the pin is fatal for the same
   reason.

3. `capture_phase_diff_at` swallowed `git diff` failures with
   `unwrap_or_default`, so an unreadable base landed `empty: true,
   files_changed: 0` — byte-identical to an honest no-op. The error is now
   recorded as `diff_error`, and an empty patch that came from a failed
   diff is no longer trusted to mean an unchanged tree.

Adds scripts/verify-mission-delivery.sh, which found #1 and #2 on its
first real run. Its probes are fail-closed: no placeholder values, a
self-test that proves the uid probe can detect the split it looks for,
and FAIL-NORUN for a scenario that never executed. Its own first version
had this bug too — a `die` inside `$(...)` exited the subshell, so a run
that could not authenticate printed "all checks passed" and exited 0.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-04 18:01:02 -07:00
co-authored by Claude Opus 5
parent bb274d08c6
commit 1253595ba7
5 changed files with 597 additions and 38 deletions
+26 -22
View File
@@ -228,31 +228,35 @@ pub async fn on_launch(
// is a PathBuf the prop-schema won't expose — see provision_claw), so
// we patch the shared config file directly on the per-mission runtime
// container. The daemon picks it up on the same reload that surfaces
// the freshly-provisioned claws for the run. Non-fatal: without the
// pin, agents still write (to the sandbox) but the committer can't
// find the changes in /mission/repo.
// the freshly-provisioned claws for the run.
//
// FATAL, deliberately. This was "non-fatal: agents still write (to the
// sandbox) but the committer can't find the changes in /mission/repo" —
// which is to say, the mission runs to completion and delivers nothing.
// Mission `019fcf62` did exactly that: the pin failed with `argument list
// too long`, one line of stderr scrolled past, and phase 0 reported
// `completed` with zero files, no commit error and no push error. A launch
// that cannot bind its agents to the repo has no path to delivering work,
// so it must fail at launch where someone is still looking.
if !provisioned_claws.is_empty() && mission_gateway.is_some() {
if let Some(mp) = crate::mission_runtime::MissionRuntimeProvisioner::from_env() {
match mp
.pin_agent_workspaces(mission_id, &provisioned_claws, "/mission/repo")
mp.pin_agent_workspaces(mission_id, &provisioned_claws, "/mission/repo")
.await
{
Ok(()) => {
// The daemon reads config ONCE at boot and never re-reads
// the file, so the pin is invisible until it restarts. Its
// agents were created through its own config API, so they
// are already persisted to the file and survive the
// restart; the pairing code is re-minted on every launch.
if let Err(e) = mp.restart_container(mission_id).await {
eprintln!(
"mission_orchestrator: restart runtime for {mission_id} failed (continuing, workspace pin will not apply): {e}"
);
}
}
Err(e) => eprintln!(
"mission_orchestrator: pin workspaces for mission {mission_id} failed (continuing): {e}"
),
}
.map_err(|e| {
format!(
"could not pin agent workspaces to /mission/repo ({e}) — the mission \
would run with its agents writing to their sandboxes, delivering nothing"
)
})?;
// The daemon reads config ONCE at boot and never re-reads the
// file, so the pin is invisible until it restarts. Its agents were
// created through its own config API, so they are already
// persisted to the file and survive the restart; the pairing code
// is re-minted on every launch. Equally fatal: an unrestarted
// daemon is an unpinned daemon.
mp.restart_container(mission_id).await.map_err(|e| {
format!("could not restart the runtime to apply the workspace pin: {e}")
})?;
}
}