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
+10 -2
View File
@@ -39,6 +39,10 @@ pub struct Mission {
pub runtime_container_name: Option<String>,
/// Gateway URL the topology_worker dials for this mission's runs.
pub runtime_endpoint: Option<String>,
/// One-time pairing code captured from the fresh gateway's startup
/// log. topology_worker uses it to lazy-pair with this specific
/// mission runtime instead of the shared-runtime env token.
pub runtime_pairing_code: Option<String>,
#[serde(with = "time::serde::rfc3339")]
pub created_at: OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
@@ -186,7 +190,7 @@ pub async fn get(pool: &PgPool, id: Uuid, workspace_id: Uuid) -> Result<Option<M
"SELECT id, workspace_id, title, template_kind, team_id,
team_template_id, repo_id, schedule, status,
description, config, runtime_kind, target_node_id,
runtime_container_name, runtime_endpoint,
runtime_container_name, runtime_endpoint, runtime_pairing_code,
created_at, updated_at, completed_at
FROM missions WHERE id = $1 AND workspace_id = $2",
)
@@ -227,7 +231,7 @@ pub async fn list_by_workspace(
"SELECT id, workspace_id, title, template_kind, team_id,
team_template_id, repo_id, schedule, status,
description, config, runtime_kind, target_node_id,
runtime_container_name, runtime_endpoint,
runtime_container_name, runtime_endpoint, runtime_pairing_code,
created_at, updated_at, completed_at
FROM missions WHERE workspace_id = $1
ORDER BY created_at DESC LIMIT $2",
@@ -254,6 +258,7 @@ pub async fn list_by_workspace(
target_node_id: r.get("target_node_id"),
runtime_container_name: r.get("runtime_container_name"),
runtime_endpoint: r.get("runtime_endpoint"),
runtime_pairing_code: r.get("runtime_pairing_code"),
created_at: r.get("created_at"),
updated_at: r.get("updated_at"),
completed_at: r.get("completed_at"),
@@ -315,11 +320,13 @@ pub async fn set_runtime_binding(
workspace_id: Uuid,
container_name: Option<&str>,
endpoint: Option<&str>,
pairing_code: Option<&str>,
) -> Result<(), DbError> {
sqlx::query(
"UPDATE missions
SET runtime_container_name = $3,
runtime_endpoint = $4,
runtime_pairing_code = $5,
updated_at = now()
WHERE id = $1 AND workspace_id = $2",
)
@@ -327,6 +334,7 @@ pub async fn set_runtime_binding(
.bind(workspace_id)
.bind(container_name)
.bind(endpoint)
.bind(pairing_code)
.execute(pool)
.await?;
Ok(())