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:
co-authored by
Claude Opus 5
parent
6926107e4f
commit
b1bdfbbf87
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user