missions: schema + provisioner skeleton for per-mission runtime containers (C3 slice 1)
ci / gates (push) Successful in 7s
ci / rust (push) Failing after 14s
ci / frontend (push) Successful in 29s
ci / e2e (push) Skipped
ci / publish (push) Skipped

- migration 0058: adds missions.runtime_container_name + runtime_endpoint
- new mission_runtime module (bollard): ensure_container /
  teardown_container. Container is spawned on clawmates_core +
  clawmates_edge networks with just /var/lib/clawmates-missions/{id}
  bind-mounted so agents scoped to /mission/repo can only see this
  missions repo.
- provider API keys forwarded from the server envs so per-mission
  runtimes inherit them.
- Mission struct + repo helpers updated for the two new columns +
  set_runtime_binding().
- Unit tests cover container naming determinism + entropy.

Not wired to the orchestrator yet — that lands in slice 2.
This commit is contained in:
Omar Sobh
2026-07-21 22:17:41 -07:00
parent e5c0e5ec1a
commit 5d24fd3460
4 changed files with 320 additions and 0 deletions
+38
View File
@@ -34,6 +34,11 @@ pub struct Mission {
pub runtime_kind: String,
/// FK → nodes(id); only relevant when runtime_kind = 'local_herdr'
pub target_node_id: Option<Uuid>,
/// Per-mission ZeroClaw runtime container name (C3 workspace isolation).
/// Null until `mission_runtime::ensure_container` provisions it.
pub runtime_container_name: Option<String>,
/// Gateway URL the topology_worker dials for this mission's runs.
pub runtime_endpoint: Option<String>,
#[serde(with = "time::serde::rfc3339")]
pub created_at: OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
@@ -181,6 +186,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,
created_at, updated_at, completed_at
FROM missions WHERE id = $1 AND workspace_id = $2",
)
@@ -202,6 +208,8 @@ pub async fn get(pool: &PgPool, id: Uuid, workspace_id: Uuid) -> Result<Option<M
config: r.get("config"),
runtime_kind: r.get("runtime_kind"),
target_node_id: r.get("target_node_id"),
runtime_container_name: r.get("runtime_container_name"),
runtime_endpoint: r.get("runtime_endpoint"),
created_at: r.get("created_at"),
updated_at: r.get("updated_at"),
completed_at: r.get("completed_at"),
@@ -219,6 +227,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,
created_at, updated_at, completed_at
FROM missions WHERE workspace_id = $1
ORDER BY created_at DESC LIMIT $2",
@@ -243,6 +252,8 @@ pub async fn list_by_workspace(
config: r.get("config"),
runtime_kind: r.get("runtime_kind"),
target_node_id: r.get("target_node_id"),
runtime_container_name: r.get("runtime_container_name"),
runtime_endpoint: r.get("runtime_endpoint"),
created_at: r.get("created_at"),
updated_at: r.get("updated_at"),
completed_at: r.get("completed_at"),
@@ -294,6 +305,33 @@ pub async fn update_meta(
Ok(())
}
/// Bind a mission to its per-mission runtime container + endpoint.
/// Called by `mission_runtime::ensure_container` after the docker
/// container is running. Null endpoint clears the binding (used by
/// the teardown sweeper).
pub async fn set_runtime_binding(
pool: &PgPool,
id: Uuid,
workspace_id: Uuid,
container_name: Option<&str>,
endpoint: Option<&str>,
) -> Result<(), DbError> {
sqlx::query(
"UPDATE missions
SET runtime_container_name = $3,
runtime_endpoint = $4,
updated_at = now()
WHERE id = $1 AND workspace_id = $2",
)
.bind(id)
.bind(workspace_id)
.bind(container_name)
.bind(endpoint)
.execute(pool)
.await?;
Ok(())
}
/// Hard-delete a mission. Cascades via FKs on mission_phases /
/// mission_tasks / mission_artifacts / benchmark_snapshots (all
/// declared ON DELETE CASCADE in 0047).