fix(mission_runtime): per-mission auto-pair via container log scrape (C3 auth)
ci / gates (push) Successful in 10s
ci / rust (push) Failing after 23s
ci / frontend (push) Successful in 38s
ci / e2e (push) Skipped
ci / publish (push) Skipped

The seed-mount approach didnt work: even with the shared runtimes
data dir bind-mounted, a fresh gateway instance mints a new pairing
key and requires re-pairing. The topology_worker connect returned
401 forever.

New approach — per-mission gateways self-pair:
- Provisioner tails container logs after start, extracts the
  X-Pairing-Code from the boot banner
- Persists it on missions.runtime_pairing_code (migration 0059)
- topology_worker constructs ZeroClawDriveExecutor with THAT code
  via from_env_for_gateway_with_code, which triggers the lazy
  /pair handshake on first turn and caches the returned bearer

Drops the shared-runtime data-dir mount — each per-mission gateway
now owns its own state, restoring the C3 isolation guarantee.
This commit is contained in:
Omar Sobh
2026-07-22 13:06:25 -07:00
parent 0210f5bf51
commit b569688e04
7 changed files with 173 additions and 44 deletions
+26
View File
@@ -108,6 +108,32 @@ impl ZeroClawDriveExecutor {
Ok(exec)
}
/// Like [`from_env_for_gateway`] but with a caller-supplied pairing
/// code — used by per-mission runtimes whose fresh daemons mint a
/// new one-time code at startup. The env-derived ZEROCLAW_TOKEN
/// is ignored (belongs to the shared runtime) so the lazy pair
/// path runs and issues a bearer for this specific gateway.
pub fn from_env_for_gateway_with_code(
gateway_url: String,
pairing_code: String,
) -> Result<Self, String> {
if pairing_code.is_empty() {
return Err("empty pairing_code".to_string());
}
let default_alias =
std::env::var("ZEROCLAW_DEFAULT_AGENT").unwrap_or_else(|_| "scout".to_string());
let role_aliases = std::env::var("ZEROCLAW_AGENT_MAP")
.ok()
.map(|s| parse_agent_map(&s))
.unwrap_or_default();
Ok(Self::new(
gateway_url,
pairing_code,
role_aliases,
default_alias,
))
}
fn alias_for(&self, role: &str) -> String {
self.role_aliases
.get(role)