fix(provision): let callers declare write access instead of guessing from the role name

default_risk_profile_for_role decides whether a claw gets file edits, git and
shell by substring-matching its role against a fixed keyword list. On the
planner path that role string is free text the model invented for this
proposal, so a model's choice of wording silently decided tool access: a
proposed "implementation_lead" matches no keyword, lands research_readonly,
and then fails every file edit for a reason invisible from the role name.

TeamMemberInput and the planner's member schema now carry `needs_write`, and
resolve_risk_profile prefers it over the guess. The planner prompt asks for it
per member and says to grant write only to members that produce code or
commits. Absent (older clients, autoprovision, a model that omitted the field)
falls back to the old guess, so nothing changes for callers that don't set it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-30 10:53:08 -07:00
co-authored by Claude Opus 5
parent 6926107e4f
commit b1bdfbbf87
3 changed files with 72 additions and 6 deletions
+11 -2
View File
@@ -31,8 +31,11 @@ ALWAYS respond with STRICT JSON ONLY (no prose, no markdown), exactly: \
{\"reply\":\"<concise message to the user>\",\"proposal\":null|{\"team_name\":\"...\",\
\"topology_kind\":\"hub_spoke\",\"schedule\":null|{\"cron\":\"0 2 * * *\",\"prompt\":\"...\"},\
\"members\":[{\"name\":\"...\",\"role\":\"...\",\"model\":\"...\",\"brain_query\":\"...\",\
\"system_prompt\":\"...\",\"rationale\":\"...\"}]}}. Set proposal to null while still clarifying; include \
it once you have a concrete team. \n\nMODELS (set each member's \"model\" to exactly one token):\n\
\"system_prompt\":\"...\",\"needs_write\":true|false,\"rationale\":\"...\"}]}}. Set proposal to null while \
still clarifying; include it once you have a concrete team. \n\n\
ACCESS: set \"needs_write\" per member. true grants file edits, git and shell; false is read-only \
research tools. Grant write only to members that actually produce code or commits — the rest read-only.\n\
\n\nMODELS (set each member's \"model\" to exactly one token):\n\
- claude — Claude Opus 4.8: strongest reasoning/planning; coordinators, hard analysis. Highest cost.\n\
- glm-4.7 — strong general reasoning (Z.ai); best cost/quality default for most workers.\n\
- glm-5.2 — GLM Opus-class for the hardest reasoning roles; higher cost.\n\
@@ -157,6 +160,11 @@ pub struct ScaffoldMember {
pub brain_query: String,
#[serde(default)]
pub system_prompt: String,
/// Whether this member edits files / runs git, as declared by the planner.
/// Absent (older clients, or a model that omitted it) falls back to the
/// role-name guess in `RuntimeProvisioner::resolve_risk_profile`.
#[serde(default)]
pub needs_write: Option<bool>,
}
#[derive(Deserialize)]
pub struct ScaffoldSchedule {
@@ -213,6 +221,7 @@ pub async fn planner_scaffold(
model: if m.model.trim().is_empty() { "claude".to_string() } else { m.model.clone() },
system_prompt: m.system_prompt.clone(),
accent: String::new(),
needs_write: m.needs_write,
}).collect();
let lifecycle = lifecycle_for(&body.mode);
let (team_id, claw_ids) = match crate::routes::teams::build_team_with_lifecycle(&state, user.workspace_id, user.user_id, &body.team_name, &body.topology_kind, &members, lifecycle).await {