fix(claws): point per-mission workspace at /mission/repo + tool-inventory preamble
ci / gates (push) Successful in 18s
ci / frontend (push) Successful in 53s
ci / rust (push) Successful in 3m30s
ci / e2e (push) Skipped
ci / publish (push) Successful in 4m6s

Two stacked issues after risk_profile was fixed:

1. Claws had file_edit + 46 other tools available, but the templates
   trained the agents to expect file_read/file_write (older ZeroClaw
   tool names). Result: agent output kept saying "I only have file_read"
   and dumped implementations into the context window as text.

2. Even with file_edit, the sandbox pointed at
   /zeroclaw-data/.zeroclaw/agents/<alias>/workspace/ — NOT
   /mission/repo where the checked-out mission repo actually lives.
   unrestricted_filesystem=false blocked agents from reaching it.

Fixes:
- provision_claw now takes workspace_path. mission_orchestrator passes
  /mission/repo — pins the per-claw workspace via
  agents.<alias>.workspace.path to the bind-mount path so file_edit /
  content_search / glob_search operate on the mission's git checkout.
- phase_task_text prepends an explicit tool inventory (file_edit,
  content_search, glob_search, git_operations, git_forge, ...) plus a
  WORKSPACE line pinned at /mission/repo. Each phase directive is
  rewritten to reference file_edit / git_operations explicitly and to
  call out "do NOT paste code in your reply expecting the platform to
  save it."
This commit is contained in:
Omar Sobh
2026-07-24 13:14:30 -07:00
parent 84572186e9
commit 6d5e7c87d7
4 changed files with 73 additions and 15 deletions
+11 -1
View File
@@ -333,8 +333,18 @@ async fn mint_team_from_template(
// etc.). Passing "toolfree" — the old default — left every // etc.). Passing "toolfree" — the old default — left every
// agent with zero tools regardless of what its prompt asked for. // agent with zero tools regardless of what its prompt asked for.
if let Some(p) = provisioner { 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 if let Err(e) = p
.provision_claw(claw_id, default_model, &template.template.risk_profile) .provision_claw(
claw_id,
default_model,
&template.template.risk_profile,
Some("/mission/repo"),
)
.await .await
{ {
eprintln!( eprintln!(
+40 -12
View File
@@ -265,37 +265,65 @@ async fn launch_phase(
fn phase_task_text(kind: &str, title: &str, description: Option<&str>) -> String { fn phase_task_text(kind: &str, title: &str, description: Option<&str>) -> String {
let base = description.unwrap_or("").trim(); let base = description.unwrap_or("").trim();
// The prior template-derived system prompts trained agents to look
// for `file_read`/`file_write` — tools that no longer exist under
// ZeroClaw v0.8+. The current toolset uses `file_edit` (create /
// overwrite / patch) plus `content_search`/`glob_search`. Injecting
// the real tool inventory + concrete workspace path stops the agent
// from hallucinating "I only have file_read" and dumping the entire
// implementation into the context window instead of onto disk.
let tool_preamble = "\
TOOLS AVAILABLE (use these exact names — do NOT assume older tool names like file_read / file_write / bash exist):\n\
- file_edit — create, overwrite, or patch files in your workspace\n\
- content_search — grep across your workspace (regex on file contents)\n\
- glob_search — find files by path glob\n\
- git_operations — git status / add / commit / diff / log\n\
- git_forge — Gitea PR / branch / issue operations\n\
- web_search_tool / web_fetch — external references (research-tier profiles only)\n\
- spawn_subagent — hand off a subtask to another claw\n\
- delegate — call a peer role by name\n\
- memory_store / memory_recall — durable per-agent notes\n\
\n\
WORKSPACE: Your working directory is /mission/repo. That path is the\n\
mission's git checkout. All file_edit / content_search / glob_search\n\
operations resolve there. To read a file: file_edit with mode='read'\n\
or content_search first, then file_edit to patch. Write your outputs\n\
as REAL files with file_edit — do NOT paste code blocks in your reply\n\
expecting the platform to save them; nothing else writes files for you.\n";
let directive = match kind { let directive = match kind {
"research" => { "research" => {
"Your team is running the RESEARCH phase of this mission. \ "Your team is running the RESEARCH phase of this mission. \
Investigate the topic, gather sources, and produce a \ Investigate the topic, gather sources, and produce a \
sectioned Markdown brief the coding phase can implement \ sectioned Markdown brief the coding phase can implement \
directly. Save findings to the Obsidian vault or the \ directly. Save findings under /mission/repo/research/ \
mission's artifact directory. Emit INT-XX task markers \ using file_edit — one Markdown file per topic. Emit INT-XX \
for concrete follow-ups." task markers in the last file for concrete follow-ups."
} }
"coding" => { "coding" => {
"Your team is running the CODING phase of this mission. \ "Your team is running the CODING phase of this mission. \
Implement the mission's acceptance criteria against the \ Implement the mission's acceptance criteria against the \
checked-out repo. Follow the workspace-repo-commit-protocol: \ /mission/repo checkout using file_edit for every source \
small focused commits with test coverage. Emit COMPLETED: <INT-id> \ file, then git_operations to commit small focused changes \
markers as you close research-produced tasks." with test coverage. Emit COMPLETED: <INT-id> markers as you \
close research-produced tasks. Do NOT respond with source \
code in text — write it as files."
} }
"benchmark" => { "benchmark" => {
"Your team is running the BENCHMARK phase of this mission. \ "Your team is running the BENCHMARK phase of this mission. \
Author or extend benchmarks that measure the target change. \ Author or extend benchmarks under /mission/repo/benches or \
Baseline the pre-change performance, apply the change (or \ the crate's bench harness using file_edit. Baseline the \
use the mission's committed diff), then measure after." pre-change performance, apply the change (or use the \
mission's committed diff), then measure after."
} }
"security_scan" => { "security_scan" => {
"Your team is running the SECURITY SCAN phase of this mission. \ "Your team is running the SECURITY SCAN phase of this mission. \
Run cargo-audit, gitleaks, trivy, and semgrep against the \ Run cargo-audit, gitleaks, trivy, and semgrep against \
checked-out repo. Triage findings, file INT-XX task markers \ /mission/repo. Triage findings, file INT-XX task markers \
for remediation, propose patches for the coding phase." for remediation, propose patches for the coding phase."
} }
_ => "Execute this mission phase according to the mission brief.", _ => "Execute this mission phase according to the mission brief.",
}; };
format!("MISSION: {title}\n\n{directive}\n\nBRIEF:\n{base}") format!("MISSION: {title}\n\n{tool_preamble}\n{directive}\n\nBRIEF:\n{base}")
} }
/// Close phases whose topology_runs are all terminal. /// Close phases whose topology_runs are all terminal.
+4 -1
View File
@@ -130,8 +130,11 @@ pub(crate) async fn build_team_with_lifecycle(
.await?; .await?;
let claw_id = agent.id.as_uuid(); let claw_id = agent.id.as_uuid();
let risk = RuntimeProvisioner::default_risk_profile_for_role(&m.role); let risk = RuntimeProvisioner::default_risk_profile_for_role(&m.role);
// No workspace override on the ad-hoc team-wizard path — those
// teams aren't mission-bound so they use the default per-agent
// workspace under <install>/agents/<alias>/workspace/.
provisioner provisioner
.provision_claw(claw_id, &m.model, risk) .provision_claw(claw_id, &m.model, risk, None)
.await .await
.map_err(|e| { .map_err(|e| {
eprintln!("teams: provision claw {claw_id} failed: {e}"); eprintln!("teams: provision claw {claw_id} failed: {e}");
+18 -1
View File
@@ -136,12 +136,22 @@ impl RuntimeProvisioner {
/// `risk_profile` (from the team template — controls which tools this /// `risk_profile` (from the team template — controls which tools this
/// agent gets: `toolfree` = nothing, `research_readonly` = file_read /// agent gets: `toolfree` = nothing, `research_readonly` = file_read
/// only, `coding_readwrite` = file_read + file_write + shell, etc.), /// only, `coding_readwrite` = file_read + file_write + shell, etc.),
/// and the `clawmates_door` MCP bundle. Idempotent on the create step. /// and the `clawmates_door` MCP bundle.
///
/// `workspace_path`, when Some, pins the agent's per-agent workspace
/// dir via `[agents.<alias>.workspace.path]`. In per-mission runtime
/// containers this is `/mission/repo` so `file_edit` / `content_search`
/// / `glob_search` operate on the mission's checked-out repo instead
/// of the default `<install>/agents/<alias>/workspace/` sandbox
/// (which the agent can't populate with the mission's source files).
///
/// Idempotent on the create step.
pub async fn provision_claw( pub async fn provision_claw(
&self, &self,
claw_id: Uuid, claw_id: Uuid,
model: &str, model: &str,
risk_profile: &str, risk_profile: &str,
workspace_path: Option<&str>,
) -> Result<String, String> { ) -> Result<String, String> {
let alias = claw_alias(claw_id); let alias = claw_alias(claw_id);
let model_alias = provider_alias_for(model); let model_alias = provider_alias_for(model);
@@ -179,6 +189,13 @@ impl RuntimeProvisioner {
serde_json::json!(["clawmates_door"]), serde_json::json!(["clawmates_door"]),
) )
.await?; .await?;
if let Some(path) = workspace_path {
self.set_prop(
&format!("agents.{alias}.workspace.path"),
serde_json::json!(path),
)
.await?;
}
Ok(alias) Ok(alias)
} }