fix(missions): provision claws into the mission's own daemon + reload the pin

Mission turns execute against the per-mission runtime container, but claws
were provisioned via RuntimeProvisioner::from_env() — i.e. the GLOBAL gateway.
That daemon loads config once at boot and never re-reads the file, so the
per-mission daemon had no claw_* agents at all: querying it for a mission
claw's risk_profile returned 404 while the global daemon returned 200. With
the alias unresolvable, the daemon silently fell back to the default `scout`
agent, which is jailed to the global workspace — agents reported "the scout
agent workspace" and "/mission/repo isn't accessible", produced no files, and
burned tokens. This is the deeper cause behind the empty-output runs; the
tool-allowlist and workspace-pin fixes were necessary but not sufficient.

- RuntimeProvisioner::for_gateway(url) — aim the provisioner at a specific
  gateway (mirrors ZeroClawDriveExecutor::from_env_for_gateway); from_env now
  delegates to it.
- mission_orchestrator captures the per-mission endpoint from ensure_container
  and provisions every claw there, falling back to the global gateway only
  when there is no per-mission runtime (dev/no-docker).
- workspace.path is file-only (the config prop API cannot set a PathBuf), and
  the daemon never re-reads the file, so pin_agent_workspaces is now followed
  by restart_container(): restart + wait for /health to answer. Agents created
  through the daemon's own config API are already persisted to that file, so
  they survive; the pairing code is re-minted on every launch.
  The readiness probe inspects the /health BODY — exec_capture only fails on
  docker errors, so a curl that cannot connect still "succeeds".

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-28 10:27:54 +02:00
co-authored by Claude Opus 4.8
parent bf32da949f
commit 06ae608d0c
3 changed files with 89 additions and 7 deletions
+34 -6
View File
@@ -11,7 +11,13 @@
//! against the world) is layered on top by Slices 58.
//!
//! Design notes:
//! - Runtime provisioning is opt-in via `RuntimeProvisioner::from_env`.
//! - Runtime provisioning is opt-in. Claws are provisioned against the
//! mission's OWN daemon (`RuntimeProvisioner::for_gateway` with the
//! per-mission endpoint), falling back to the global gateway only when
//! there is no per-mission runtime. Provisioning into the global gateway
//! while the run executes on a per-mission daemon leaves that daemon
//! without the `claw_*` agents — it falls back to the default `scout`
//! agent, which cannot see `/mission/repo`.
//! Missing runtime = "insert DB rows only, no live claw" — the
//! mission still boots; live claws land the moment the runtime
//! env is configured + the mission re-launches.
@@ -70,9 +76,13 @@ pub async fn on_launch(
// running. Falls back silently when docker is unreachable so
// dev-mode + tests still work — the topology_worker will use the
// shared runtime endpoint in that case.
// The mission's own runtime endpoint. Claws MUST be provisioned against
// THIS gateway, not the global one — see RuntimeProvisioner::for_gateway.
let mut mission_gateway: Option<String> = None;
if let Some(prov) = crate::mission_runtime::MissionRuntimeProvisioner::from_env() {
match prov.ensure_container(mission_id).await {
Ok(ec) => {
mission_gateway = Some(ec.endpoint.clone());
let container_name = crate::mission_runtime::container_name(mission_id);
if let Err(e) = cm_db::repo::missions::set_runtime_binding(
pool,
@@ -157,7 +167,13 @@ pub async fn on_launch(
);
}
let provisioner = RuntimeProvisioner::from_env();
// Provision into the mission's own daemon when we have one (so the daemon
// that actually runs the turns knows these claws); fall back to the global
// gateway only for dev/no-docker setups where the run uses it too.
let provisioner = match mission_gateway.clone() {
Some(url) => RuntimeProvisioner::for_gateway(url),
None => RuntimeProvisioner::from_env(),
};
let mut first_team_id: Option<Uuid> = None;
let mut provisioned_claws: Vec<cm_domain::AgentId> = Vec::new();
for (purpose, template_id) in &picks {
@@ -213,15 +229,27 @@ pub async fn on_launch(
// 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.
if !provisioned_claws.is_empty() {
if !provisioned_claws.is_empty() && mission_gateway.is_some() {
if let Some(mp) = crate::mission_runtime::MissionRuntimeProvisioner::from_env() {
if let Err(e) = mp
match mp
.pin_agent_workspaces(mission_id, &provisioned_claws, "/mission/repo")
.await
{
eprintln!(
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}"
);
),
}
}
}