fix(missions): grant coding tools + pin claw workspace to /mission/repo

Mission agents were burning ~275K tokens producing nothing: the coder had
only file_read and its workspace was the empty ephemeral sandbox, so it
dumped a full spec inline instead of writing files. Two root causes:

1. Risk-profile allowlists used pre-0.8 tool names. `coding_readwrite`
   allow-listed `file_write` (renamed to `file_edit` in ZeroClaw 0.8, and
   `file_write` now refuses on ephemeral workspaces) and omitted file_edit
   / content_search / glob_search / git_operations — the exact tools the
   phase prompt tells agents to use. Since allowed_tools is a strict
   allowlist, agents were effectively read-only. Documents the correct
   profiles in agent.config.example.toml (they only lived in host config;
   the live runtime profiles were corrected via its config API).

2. workspace.path never got set. `agents.<alias>.workspace.path` is an
   Option<PathBuf> the ZeroClaw Configurable macro skips from prop
   enumeration, so provision_claw's set_prop always 404'd and the whole
   call errored into a swallowed eprintln. Removes the dead set_prop and
   pins the workspace out-of-band: MissionRuntimeProvisioner::
   pin_agent_workspaces patches the shared config file on the per-mission
   container (format-preserving via toml_edit, atomic temp+mv); the daemon
   applies it on the same reload that surfaces the freshly-provisioned
   claws. Covered by unit tests for the TOML stamp.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-28 08:40:18 +02:00
co-authored by Claude Opus 4.8
parent 6d5e7c87d7
commit 34409bca0c
7 changed files with 279 additions and 35 deletions
+36 -14
View File
@@ -159,6 +159,7 @@ pub async fn on_launch(
let provisioner = 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 {
let template = cm_db::repo::team_templates::get(pool, *template_id)
.await
@@ -176,6 +177,7 @@ pub async fn on_launch(
&template,
&team_name,
"claude-sonnet-5",
&mut provisioned_claws,
)
.await?;
// Record (mission, team, purpose) in mission_teams so the Team
@@ -201,6 +203,29 @@ pub async fn on_launch(
.await
.map_err(|e| format!("bind team on mission: {e}"))?;
// Pin every provisioned claw's workspace to /mission/repo so
// file_edit / content_search / glob_search / git_operations operate
// on the mission's checked-out repo instead of the empty per-agent
// sandbox. This CANNOT go through the config prop API (workspace.path
// is a PathBuf the prop-schema won't expose — see provision_claw), so
// we patch the shared config file directly on the per-mission runtime
// container. The daemon picks it up on the same reload that surfaces
// 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 let Some(mp) = crate::mission_runtime::MissionRuntimeProvisioner::from_env() {
if let Err(e) = mp
.pin_agent_workspaces(mission_id, &provisioned_claws, "/mission/repo")
.await
{
eprintln!(
"mission_orchestrator: pin workspaces for mission {mission_id} failed (continuing): {e}"
);
}
}
}
// Herdr second-runtime: if runtime_kind='local_herdr', spawn a
// pane on target_node running the first available local CLI.
// Non-fatal on failure — the operator sees the error in server
@@ -253,6 +278,7 @@ async fn mint_team_from_template(
template: &TeamTemplateDetail,
team_name: &str,
default_model: &str,
provisioned_claws: &mut Vec<cm_domain::AgentId>,
) -> Result<Uuid, String> {
// Build the topology graph from role slots so the team's `graph`
// NOT NULL column is satisfied + downstream topology executors
@@ -332,24 +358,20 @@ async fn mint_team_from_template(
// scout/researcher, coding_readwrite for coder/tester/committer,
// etc.). Passing "toolfree" — the old default — left every
// agent with zero tools regardless of what its prompt asked for.
//
// Workspace pinning to /mission/repo is NOT done here (the
// config prop-schema can't set workspace.path — see
// provision_claw's doc); the caller pins the collected claws
// out-of-band via MissionRuntimeProvisioner::pin_agent_workspaces.
if let Some(p) = provisioner {
// /mission/repo is the bind-mount path inside the per-mission
// runtime container (see mission_runtime::ensure_container).
// Pinning workspace.path there lets file_edit / glob_search /
// content_search actually operate on the mission's checked-out
// repo instead of the empty per-agent sandbox.
if let Err(e) = p
.provision_claw(
claw_id,
default_model,
&template.template.risk_profile,
Some("/mission/repo"),
)
match p
.provision_claw(claw_id, default_model, &template.template.risk_profile)
.await
{
eprintln!(
Ok(_) => provisioned_claws.push(agent.id),
Err(e) => eprintln!(
"mission_orchestrator: provision claw {claw_id} failed (continuing): {e}"
);
),
}
}