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:
co-authored by
Claude Opus 4.8
parent
bf32da949f
commit
06ae608d0c
@@ -11,7 +11,13 @@
|
||||
//! against the world) is layered on top by Slices 5–8.
|
||||
//!
|
||||
//! 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}"
|
||||
);
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user