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:
co-authored by
Claude Opus 4.8
parent
6d5e7c87d7
commit
34409bca0c
Generated
+1
@@ -974,6 +974,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite 0.26.2",
|
"tokio-tungstenite 0.26.2",
|
||||||
"toml",
|
"toml",
|
||||||
|
"toml_edit",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ publish.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
getrandom = "0.2"
|
getrandom = "0.2"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
toml_edit = "0.22"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ pub async fn on_launch(
|
|||||||
|
|
||||||
let provisioner = RuntimeProvisioner::from_env();
|
let provisioner = RuntimeProvisioner::from_env();
|
||||||
let mut first_team_id: Option<Uuid> = None;
|
let mut first_team_id: Option<Uuid> = None;
|
||||||
|
let mut provisioned_claws: Vec<cm_domain::AgentId> = Vec::new();
|
||||||
for (purpose, template_id) in &picks {
|
for (purpose, template_id) in &picks {
|
||||||
let template = cm_db::repo::team_templates::get(pool, *template_id)
|
let template = cm_db::repo::team_templates::get(pool, *template_id)
|
||||||
.await
|
.await
|
||||||
@@ -176,6 +177,7 @@ pub async fn on_launch(
|
|||||||
&template,
|
&template,
|
||||||
&team_name,
|
&team_name,
|
||||||
"claude-sonnet-5",
|
"claude-sonnet-5",
|
||||||
|
&mut provisioned_claws,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
// Record (mission, team, purpose) in mission_teams so the Team
|
// Record (mission, team, purpose) in mission_teams so the Team
|
||||||
@@ -201,6 +203,29 @@ pub async fn on_launch(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| format!("bind team on mission: {e}"))?;
|
.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
|
// Herdr second-runtime: if runtime_kind='local_herdr', spawn a
|
||||||
// pane on target_node running the first available local CLI.
|
// pane on target_node running the first available local CLI.
|
||||||
// Non-fatal on failure — the operator sees the error in server
|
// Non-fatal on failure — the operator sees the error in server
|
||||||
@@ -253,6 +278,7 @@ async fn mint_team_from_template(
|
|||||||
template: &TeamTemplateDetail,
|
template: &TeamTemplateDetail,
|
||||||
team_name: &str,
|
team_name: &str,
|
||||||
default_model: &str,
|
default_model: &str,
|
||||||
|
provisioned_claws: &mut Vec<cm_domain::AgentId>,
|
||||||
) -> Result<Uuid, String> {
|
) -> Result<Uuid, String> {
|
||||||
// Build the topology graph from role slots so the team's `graph`
|
// Build the topology graph from role slots so the team's `graph`
|
||||||
// NOT NULL column is satisfied + downstream topology executors
|
// 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,
|
// scout/researcher, coding_readwrite for coder/tester/committer,
|
||||||
// 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.
|
||||||
|
//
|
||||||
|
// 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 {
|
if let Some(p) = provisioner {
|
||||||
// /mission/repo is the bind-mount path inside the per-mission
|
match p
|
||||||
// runtime container (see mission_runtime::ensure_container).
|
.provision_claw(claw_id, default_model, &template.template.risk_profile)
|
||||||
// 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"),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
eprintln!(
|
Ok(_) => provisioned_claws.push(agent.id),
|
||||||
|
Err(e) => eprintln!(
|
||||||
"mission_orchestrator: provision claw {claw_id} failed (continuing): {e}"
|
"mission_orchestrator: provision claw {claw_id} failed (continuing): {e}"
|
||||||
);
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ use bollard::query_parameters::{
|
|||||||
CreateContainerOptions, InspectContainerOptions, RemoveContainerOptions, StartContainerOptions,
|
CreateContainerOptions, InspectContainerOptions, RemoveContainerOptions, StartContainerOptions,
|
||||||
};
|
};
|
||||||
use bollard::Docker;
|
use bollard::Docker;
|
||||||
|
use base64::Engine;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -343,6 +344,131 @@ impl MissionRuntimeProvisioner {
|
|||||||
}
|
}
|
||||||
extract_pairing_code_from_json(&buf)
|
extract_pairing_code_from_json(&buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stamp `[agents.<alias>.workspace] path = "<workspace_path>"` into the
|
||||||
|
/// shared runtime config for each provisioned claw, by editing the
|
||||||
|
/// config file directly on the per-mission container. This is the
|
||||||
|
/// out-of-band path for workspace pinning: the ZeroClaw config prop API
|
||||||
|
/// cannot set `workspace.path` (an `Option<PathBuf>` the `Configurable`
|
||||||
|
/// macro skips from prop enumeration), so `provision_claw` leaves it
|
||||||
|
/// unset and we stamp it here. The daemon applies the change on the
|
||||||
|
/// same config reload that surfaces the freshly-provisioned claws for
|
||||||
|
/// the run.
|
||||||
|
///
|
||||||
|
/// Concurrency caveat (mirrors the seed-dir note above): the config
|
||||||
|
/// file is shared across the persistent runtime and every per-mission
|
||||||
|
/// daemon, so this read-modify-write can race a provision from another
|
||||||
|
/// mission launching at the same instant. Missions launch one at a
|
||||||
|
/// time in practice; the durable fix is per-mission config isolation.
|
||||||
|
pub async fn pin_agent_workspaces(
|
||||||
|
&self,
|
||||||
|
mission_id: Uuid,
|
||||||
|
claws: &[cm_domain::AgentId],
|
||||||
|
workspace_path: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if claws.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
const CONFIG_PATH: &str = "/zeroclaw-data/.zeroclaw/config.toml";
|
||||||
|
let name = container_name(mission_id);
|
||||||
|
|
||||||
|
let raw = self
|
||||||
|
.exec_capture(&name, vec!["cat".into(), CONFIG_PATH.into()])
|
||||||
|
.await?;
|
||||||
|
let aliases: Vec<String> = claws
|
||||||
|
.iter()
|
||||||
|
.map(|c| crate::runtime_provision::claw_alias(c.as_uuid()))
|
||||||
|
.collect();
|
||||||
|
let (edited, pinned) = stamp_workspace_paths(&raw, &aliases, workspace_path)?;
|
||||||
|
if pinned == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let b64 = base64::engine::general_purpose::STANDARD.encode(edited.as_bytes());
|
||||||
|
// Decode to a sibling temp then atomically move over the live file,
|
||||||
|
// so a partial write can never leave the daemon with truncated TOML.
|
||||||
|
let script = format!(
|
||||||
|
"printf %s '{b64}' | base64 -d > {CONFIG_PATH}.tmp && mv {CONFIG_PATH}.tmp {CONFIG_PATH}"
|
||||||
|
);
|
||||||
|
let out = self
|
||||||
|
.exec_capture(&name, vec!["sh".into(), "-c".into(), script])
|
||||||
|
.await?;
|
||||||
|
if !out.trim().is_empty() {
|
||||||
|
return Err(format!("write runtime config.toml: {out}"));
|
||||||
|
}
|
||||||
|
eprintln!(
|
||||||
|
"mission_runtime: pinned {pinned} workspace(s) → {workspace_path} for mission {mission_id}"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a command in the mission container and return its combined
|
||||||
|
/// stdout+stderr as a String. Used for small config round-trips.
|
||||||
|
async fn exec_capture(&self, name: &str, cmd: Vec<String>) -> Result<String, String> {
|
||||||
|
let exec = self
|
||||||
|
.docker
|
||||||
|
.create_exec(
|
||||||
|
name,
|
||||||
|
CreateExecOptions {
|
||||||
|
cmd: Some(cmd),
|
||||||
|
attach_stdout: Some(true),
|
||||||
|
attach_stderr: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("create_exec on {name}: {e}"))?;
|
||||||
|
let started = self
|
||||||
|
.docker
|
||||||
|
.start_exec(&exec.id, None)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("start_exec on {name}: {e}"))?;
|
||||||
|
let StartExecResults::Attached { mut output, .. } = started else {
|
||||||
|
return Err(format!("exec on {name} returned a detached result"));
|
||||||
|
};
|
||||||
|
let mut buf = String::new();
|
||||||
|
while let Some(chunk) = output.next().await {
|
||||||
|
match chunk {
|
||||||
|
Ok(c) => buf.push_str(&c.to_string()),
|
||||||
|
Err(e) => return Err(format!("exec output stream on {name}: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format-preserving stamp of `[agents.<alias>.workspace] path = "<path>"`
|
||||||
|
/// for each alias present in `raw`. Keeps the operator's comments, ordering,
|
||||||
|
/// and every untouched byte intact; only the `path` keys change. Aliases not
|
||||||
|
/// already present are skipped (never fabricated — a bare agent table would
|
||||||
|
/// drop that agent's model/risk_profile/bundles). Returns the edited document
|
||||||
|
/// and how many agents were pinned.
|
||||||
|
fn stamp_workspace_paths(
|
||||||
|
raw: &str,
|
||||||
|
aliases: &[String],
|
||||||
|
workspace_path: &str,
|
||||||
|
) -> Result<(String, usize), String> {
|
||||||
|
let mut doc = raw
|
||||||
|
.parse::<toml_edit::DocumentMut>()
|
||||||
|
.map_err(|e| format!("parse runtime config.toml: {e}"))?;
|
||||||
|
let Some(agents) = doc.get_mut("agents").and_then(|i| i.as_table_like_mut()) else {
|
||||||
|
return Err("runtime config has no [agents] table".to_string());
|
||||||
|
};
|
||||||
|
let mut pinned = 0usize;
|
||||||
|
for alias in aliases {
|
||||||
|
let Some(agent) = agents.get_mut(alias).and_then(|i| i.as_table_like_mut()) else {
|
||||||
|
eprintln!("mission_runtime: pin skip — {alias} absent from config");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if agent.get("workspace").is_none() {
|
||||||
|
agent.insert("workspace", toml_edit::Item::Table(toml_edit::Table::new()));
|
||||||
|
}
|
||||||
|
if let Some(ws) = agent.get_mut("workspace").and_then(|i| i.as_table_like_mut()) {
|
||||||
|
ws.insert("path", toml_edit::value(workspace_path));
|
||||||
|
pinned += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((doc.to_string(), pinned))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the JSON `{ "pairing_code": "NNNNNN", ... }` body from the
|
/// Parse the JSON `{ "pairing_code": "NNNNNN", ... }` body from the
|
||||||
@@ -441,6 +567,76 @@ async fn sweep_once(pool: &sqlx::PgPool, grace: std::time::Duration) -> Result<(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
const SAMPLE_CONFIG: &str = r#"# top comment
|
||||||
|
[agents.claw_a]
|
||||||
|
model_provider = "anthropic.default"
|
||||||
|
risk_profile = "coding_readwrite"
|
||||||
|
mcp_bundles = ["clawmates_door"]
|
||||||
|
|
||||||
|
[agents.claw_a.workspace]
|
||||||
|
unrestricted_filesystem = false
|
||||||
|
|
||||||
|
[agents.claw_b]
|
||||||
|
risk_profile = "research_readonly"
|
||||||
|
|
||||||
|
[risk_profiles.coding_readwrite]
|
||||||
|
# keep this comment
|
||||||
|
allowed_tools = ["file_read", "file_edit"]
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stamp_pins_path_and_preserves_existing_workspace_fields() {
|
||||||
|
let (out, n) = stamp_workspace_paths(
|
||||||
|
SAMPLE_CONFIG,
|
||||||
|
&["claw_a".to_string()],
|
||||||
|
"/mission/repo",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(n, 1);
|
||||||
|
assert!(out.contains(r#"path = "/mission/repo""#));
|
||||||
|
// The sibling field in the same table is untouched.
|
||||||
|
assert!(out.contains("unrestricted_filesystem = false"));
|
||||||
|
// Comments and unrelated sections survive the round-trip.
|
||||||
|
assert!(out.contains("# top comment"));
|
||||||
|
assert!(out.contains("# keep this comment"));
|
||||||
|
assert!(out.contains("[risk_profiles.coding_readwrite]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stamp_creates_workspace_table_when_absent() {
|
||||||
|
let (out, n) =
|
||||||
|
stamp_workspace_paths(SAMPLE_CONFIG, &["claw_b".to_string()], "/mission/repo").unwrap();
|
||||||
|
assert_eq!(n, 1);
|
||||||
|
// claw_b had no [workspace] table; it now has one with the path.
|
||||||
|
let doc = out.parse::<toml_edit::DocumentMut>().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
doc["agents"]["claw_b"]["workspace"]["path"].as_str(),
|
||||||
|
Some("/mission/repo")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stamp_skips_absent_aliases_without_fabricating_them() {
|
||||||
|
let (out, n) =
|
||||||
|
stamp_workspace_paths(SAMPLE_CONFIG, &["claw_missing".to_string()], "/mission/repo")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(n, 0);
|
||||||
|
assert!(!out.contains("claw_missing"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stamp_is_idempotent_overwriting_a_prior_path() {
|
||||||
|
let once =
|
||||||
|
stamp_workspace_paths(SAMPLE_CONFIG, &["claw_a".to_string()], "/old/path").unwrap().0;
|
||||||
|
let (twice, n) =
|
||||||
|
stamp_workspace_paths(&once, &["claw_a".to_string()], "/mission/repo").unwrap();
|
||||||
|
assert_eq!(n, 1);
|
||||||
|
assert!(twice.contains(r#"path = "/mission/repo""#));
|
||||||
|
assert!(!twice.contains("/old/path"));
|
||||||
|
// Exactly one path key for claw_a (no duplication).
|
||||||
|
assert_eq!(twice.matches("path = ").count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn container_name_is_stable_and_prefixed() {
|
fn container_name_is_stable_and_prefixed() {
|
||||||
let id = Uuid::parse_str("019f84a0-f2a2-7bd0-be9b-86713ec73693").unwrap();
|
let id = Uuid::parse_str("019f84a0-f2a2-7bd0-be9b-86713ec73693").unwrap();
|
||||||
|
|||||||
@@ -130,11 +130,10 @@ 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
|
// Ad-hoc team-wizard teams aren't mission-bound, so they use the
|
||||||
// teams aren't mission-bound so they use the default per-agent
|
// default per-agent workspace under <install>/agents/<alias>/workspace/.
|
||||||
// workspace under <install>/agents/<alias>/workspace/.
|
|
||||||
provisioner
|
provisioner
|
||||||
.provision_claw(claw_id, &m.model, risk, None)
|
.provision_claw(claw_id, &m.model, risk)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
eprintln!("teams: provision claw {claw_id} failed: {e}");
|
eprintln!("teams: provision claw {claw_id} failed: {e}");
|
||||||
|
|||||||
@@ -134,16 +134,21 @@ impl RuntimeProvisioner {
|
|||||||
|
|
||||||
/// Create `claw_<id>` as a live runtime agent bound to `model_alias`,
|
/// Create `claw_<id>` as a live runtime agent bound to `model_alias`,
|
||||||
/// `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.),
|
/// content_search + glob_search, `coding_readwrite` = adds file_edit +
|
||||||
/// and the `clawmates_door` MCP bundle.
|
/// git_operations + shell, etc.; see the `[risk_profiles.*]` allowlists
|
||||||
|
/// in `deploy/clawmates-runtime/agent.config.example.toml`), and the
|
||||||
|
/// `clawmates_door` MCP bundle.
|
||||||
///
|
///
|
||||||
/// `workspace_path`, when Some, pins the agent's per-agent workspace
|
/// NOTE ON WORKSPACE PINNING: `[agents.<alias>.workspace.path]` is an
|
||||||
/// dir via `[agents.<alias>.workspace.path]`. In per-mission runtime
|
/// `Option<PathBuf>` field that the ZeroClaw config prop-schema does NOT
|
||||||
/// containers this is `/mission/repo` so `file_edit` / `content_search`
|
/// expose as settable (the `Configurable` macro skips `PathBuf` from
|
||||||
/// / `glob_search` operate on the mission's checked-out repo instead
|
/// property enumeration — `zeroclaw-macros/src/lib.rs`), so a
|
||||||
/// of the default `<install>/agents/<alias>/workspace/` sandbox
|
/// `set_prop("agents.<alias>.workspace.path", …)` here would always 404
|
||||||
/// (which the agent can't populate with the mission's source files).
|
/// with `path_not_found` and fail the whole provision. Per-mission
|
||||||
|
/// workspace pinning is therefore done out-of-band by
|
||||||
|
/// `MissionRuntimeProvisioner::pin_agent_workspaces`, which patches the
|
||||||
|
/// shared config file directly for the mission's claws.
|
||||||
///
|
///
|
||||||
/// Idempotent on the create step.
|
/// Idempotent on the create step.
|
||||||
pub async fn provision_claw(
|
pub async fn provision_claw(
|
||||||
@@ -151,7 +156,6 @@ impl RuntimeProvisioner {
|
|||||||
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);
|
||||||
@@ -189,13 +193,6 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,6 +125,34 @@ level = "full"
|
|||||||
allowed_tools = []
|
allowed_tools = []
|
||||||
excluded_tools = ["shell", "file_read", "file_write", "http_request", "browser", "composio"]
|
excluded_tools = ["shell", "file_read", "file_write", "http_request", "browser", "composio"]
|
||||||
|
|
||||||
|
# Writable coding-loop profile (coder/tester/committer/engineer roles). The
|
||||||
|
# `allowed_tools` list is a STRICT allowlist — a tool must be named here to be
|
||||||
|
# callable. These MUST be the current ZeroClaw 0.8+ tool names:
|
||||||
|
# file_edit — create/overwrite/patch (the real write tool; `file_write`
|
||||||
|
# was renamed and now REFUSES on ephemeral workspaces, so a
|
||||||
|
# stale `file_write` entry silently leaves agents read-only)
|
||||||
|
# content_search — grep across the workspace
|
||||||
|
# glob_search — find files by glob
|
||||||
|
# git_operations — git status/add/commit/diff/log
|
||||||
|
# Regression guard: if you ever see an agent report "I only have file_read" and
|
||||||
|
# burn tokens dumping code inline, this list drifted back to pre-0.8 names.
|
||||||
|
[risk_profiles.coding_readwrite]
|
||||||
|
level = "full"
|
||||||
|
allowed_tools = ["file_read", "file_edit", "content_search", "glob_search", "git_operations", "shell"]
|
||||||
|
excluded_tools = ["http_request", "browser", "composio"]
|
||||||
|
|
||||||
|
# Read-only research profile (scout/researcher/reviewer/planner roles).
|
||||||
|
[risk_profiles.research_readonly]
|
||||||
|
level = "full"
|
||||||
|
allowed_tools = ["file_read", "content_search", "glob_search"]
|
||||||
|
excluded_tools = ["shell", "file_write", "http_request", "browser", "composio"]
|
||||||
|
|
||||||
|
# Read-only research + public web (papers, docs). Still no shell / no write.
|
||||||
|
[risk_profiles.research_web_readonly]
|
||||||
|
level = "full"
|
||||||
|
allowed_tools = ["file_read", "content_search", "glob_search", "web_search", "web_fetch"]
|
||||||
|
excluded_tools = ["shell", "file_write", "http_request", "browser", "composio"]
|
||||||
|
|
||||||
# The role-cast. node.role → agent alias is configured Clawmates-side via
|
# The role-cast. node.role → agent alias is configured Clawmates-side via
|
||||||
# ZEROCLAW_AGENT_MAP (e.g. "analyst=researcher"); `scout` is the default
|
# ZEROCLAW_AGENT_MAP (e.g. "analyst=researcher"); `scout` is the default
|
||||||
# fallback (ZEROCLAW_DEFAULT_AGENT) for any unmapped role.
|
# fallback (ZEROCLAW_DEFAULT_AGENT) for any unmapped role.
|
||||||
|
|||||||
Reference in New Issue
Block a user