huskies: merge 1142 story Force coder agents through MCP-validated Edit/Write/Bash to prevent writes to master worktree

This commit is contained in:
dave
2026-05-18 16:52:49 +00:00
parent 34e78bdbd5
commit f8ff63af0e
6 changed files with 565 additions and 2 deletions
+60
View File
@@ -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");
}
}