huskies: merge 1142 story Force coder agents through MCP-validated Edit/Write/Bash to prevent writes to master worktree
This commit is contained in:
@@ -116,6 +116,23 @@ pub(super) fn maybe_inject_gate_failure(args: &mut Vec<String>, story_id: &str)
|
||||
}
|
||||
}
|
||||
|
||||
/// Append `Edit,Write,Bash` to the `--disallowedTools` flag so worktree agents
|
||||
/// cannot write to the master tree via Claude's built-in tools. If
|
||||
/// `--disallowedTools` is already present (from agent config), the three names
|
||||
/// are appended to the existing value rather than replacing it.
|
||||
pub(super) fn inject_worktree_disallowed_tools(args: &mut Vec<String>) {
|
||||
const BLOCKED: &str = "Edit,Write,Bash";
|
||||
if let Some(pos) = args.iter().position(|a| a == "--disallowedTools") {
|
||||
if let Some(val) = args.get_mut(pos + 1) {
|
||||
val.push(',');
|
||||
val.push_str(BLOCKED);
|
||||
}
|
||||
} else {
|
||||
args.push("--disallowedTools".to_string());
|
||||
args.push(BLOCKED.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the background worktree-creation + agent-launch flow.
|
||||
///
|
||||
/// Caller (`AgentPool::start_agent`) wraps this in `tokio::spawn` and stores
|
||||
@@ -264,6 +281,10 @@ pub(super) async fn run_agent_spawn(
|
||||
maybe_inject_gate_failure(&mut args, &sid);
|
||||
// Cap turns and budget for merge-gate fixup sessions (story 981).
|
||||
maybe_cap_for_merge_fixup(&mut args, &sid);
|
||||
// Every agent that runs inside a worktree must use the validated MCP
|
||||
// edit/write tools instead of Claude's built-in Edit/Write/Bash. This
|
||||
// prevents accidental writes to the master worktree (stories 1127, 1136).
|
||||
inject_worktree_disallowed_tools(&mut args);
|
||||
|
||||
// Append project-local prompt content (.huskies/AGENT.md) to the
|
||||
// baked-in prompt so every agent role sees project-specific guidance
|
||||
@@ -1297,4 +1318,43 @@ mod tests {
|
||||
item.stage().dir_name()
|
||||
);
|
||||
}
|
||||
|
||||
// ── inject_worktree_disallowed_tools (AC1, story 1142) ───────────
|
||||
|
||||
/// AC3(c) proxy: worktree agents get `--disallowedTools Edit,Write,Bash`.
|
||||
#[test]
|
||||
fn worktree_disallowed_tools_added_when_absent() {
|
||||
let mut args: Vec<String> = vec!["--verbose".to_string()];
|
||||
inject_worktree_disallowed_tools(&mut args);
|
||||
let pos = args
|
||||
.iter()
|
||||
.position(|a| a == "--disallowedTools")
|
||||
.expect("--disallowedTools must be present");
|
||||
let val = &args[pos + 1];
|
||||
assert!(val.contains("Edit"), "must include Edit");
|
||||
assert!(val.contains("Write"), "must include Write");
|
||||
assert!(val.contains("Bash"), "must include Bash");
|
||||
}
|
||||
|
||||
/// Existing `--disallowedTools` value is extended, not replaced.
|
||||
#[test]
|
||||
fn worktree_disallowed_tools_appended_to_existing() {
|
||||
let mut args = vec!["--disallowedTools".to_string(), "SomeOtherTool".to_string()];
|
||||
inject_worktree_disallowed_tools(&mut args);
|
||||
// Only one --disallowedTools flag.
|
||||
let count = args
|
||||
.iter()
|
||||
.filter(|a| a.as_str() == "--disallowedTools")
|
||||
.count();
|
||||
assert_eq!(count, 1, "must not duplicate --disallowedTools");
|
||||
let pos = args.iter().position(|a| a == "--disallowedTools").unwrap();
|
||||
let val = &args[pos + 1];
|
||||
assert!(
|
||||
val.contains("SomeOtherTool"),
|
||||
"original tool must be preserved"
|
||||
);
|
||||
assert!(val.contains("Edit"), "Edit must be added");
|
||||
assert!(val.contains("Write"), "Write must be added");
|
||||
assert!(val.contains("Bash"), "Bash must be added");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user