From f061b4e90ecbe4700a53d1c1cdf79ff5814feaff Mon Sep 17 00:00:00 2001 From: James Devine Date: Tue, 21 Apr 2026 16:29:46 +0100 Subject: [PATCH 1/3] fix: align tool allow lists with gh-aw MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Emit --allow-all-tools when bash wildcard (:* or *) is set, dropping all individual --allow-tool flags (matches gh-aw computeCopilotToolArguments) - Default to --allow-all-tools when bash is not specified (matches gh-aw's applyDefaultTools sandbox behavior — bash: [*] is the default when sandbox is enabled, and ado-aw agents always run in AWF sandbox) - Emit --allow-all-paths when edit tool is enabled (matches gh-aw GetExecutionSteps) - Remove DEFAULT_BASH_COMMANDS constant (no longer the default) - Update tests and AGENTS.md documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 16 +-- src/compile/common.rs | 274 ++++++++++++++++++++++++++-------------- tests/compiler_tests.rs | 17 +-- 3 files changed, 193 insertions(+), 114 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2afa687f..ad3d39af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -393,24 +393,20 @@ The `tools` field controls which tools are available to the agent. Both sub-fiel #### Default Bash Command Allow-list -When `tools.bash` is omitted, the agent can invoke the following shell commands: - -``` -cat, date, echo, grep, head, ls, pwd, sort, tail, uniq, wc, yq -``` +When `tools.bash` is omitted, the agent defaults to **unrestricted bash access** (`--allow-all-tools`). This matches gh-aw's sandbox behavior — since ado-aw agents always run inside the AWF sandbox, all tools are allowed by default. #### Configuring Bash Access ```yaml -# Default: safe built-in command list (bash field omitted) +# Default: unrestricted bash access (bash field omitted → --allow-all-tools) tools: edit: true -# Unrestricted bash access (use with caution) +# Explicit unrestricted bash (same as default) — also accepts "*" tools: bash: [":*"] -# Explicit command allow-list +# Explicit command allow-list (restricts to named commands only) tools: bash: ["cat", "ls", "grep", "find"] @@ -637,8 +633,10 @@ Should be replaced with the human-readable name from the front matter (e.g., "Da Additional params provided to copilot CLI. The compiler generates: - `--model ` - AI model from `engine` front matter field (default: claude-opus-4.5) - `--no-ask-user` - Prevents interactive prompts -- `--allow-tool ` - Explicitly allows specific tools (github, safeoutputs, write, shell commands like cat, date, echo, grep, head, ls, pwd, sort, tail, uniq, wc, yq) - `--disable-builtin-mcps` - Disables all built-in Copilot CLI MCPs (single flag, no argument) +- `--allow-all-tools` - When bash has wildcard (`":*"` or `"*"`), allows all tools instead of individual `--allow-tool` flags +- `--allow-tool ` - When bash is NOT wildcard, explicitly allows specific tools (github, safeoutputs, write, shell commands like cat, date, echo, grep, head, ls, pwd, sort, tail, uniq, wc, yq) +- `--allow-all-paths` - When `edit` tool is enabled (default), allows the agent to write to any file path MCP servers are handled entirely by the MCP Gateway (MCPG) and are not passed as copilot CLI params. diff --git a/src/compile/common.rs b/src/compile/common.rs index d817a230..9ec01dd5 100644 --- a/src/compile/common.rs +++ b/src/compile/common.rs @@ -458,52 +458,21 @@ pub fn validate_checkout_list(repositories: &[Repository], checkout: &[String]) Ok(()) } -/// Default bash commands allowed for agents (matches gh-aw defaults + yq) -const DEFAULT_BASH_COMMANDS: &[&str] = &[ - "cat", "date", "echo", "grep", "head", "ls", "pwd", "sort", "tail", "uniq", "wc", "yq", -]; - /// Generate copilot CLI params from front matter configuration pub fn generate_copilot_params( front_matter: &FrontMatter, extensions: &[super::extensions::Extension], ) -> Result { - let mut allowed_tools: Vec = Vec::new(); - - // Collect tool permissions from extensions (github, safeoutputs, azure-devops, etc.) - for ext in extensions { - for tool in ext.allowed_copilot_tools() { - if !allowed_tools.contains(&tool) { - allowed_tools.push(tool); - } - } - } - - // Collect tool permissions from user-defined MCP servers (sorted for deterministic output). - // Only add --allow-tool for MCPs that will actually produce an MCPG entry (i.e., - // WithOptions that have a container or url). McpConfig::Enabled(true) has no backing - // server in MCPG, so granting the permission would cause confusing runtime errors. - let mut sorted_mcps: Vec<_> = front_matter.mcp_servers.iter().collect(); - sorted_mcps.sort_by(|(a, _), (b, _)| a.cmp(b)); - for (name, config) in sorted_mcps { - // Skip servers already provided by extensions (case-insensitive to match - // generate_mcpg_config's eq_ignore_ascii_case guard for reserved names) - if allowed_tools.iter().any(|t| t.eq_ignore_ascii_case(name)) { - continue; - } - // Only add MCPs that have a backing server (container or url) - let has_backing_server = match config { - crate::compile::types::McpConfig::Enabled(_) => false, - crate::compile::types::McpConfig::WithOptions(opts) => { - opts.enabled.unwrap_or(true) - && (opts.container.is_some() || opts.url.is_some()) - } - }; - if !has_backing_server { - continue; - } - allowed_tools.push(name.clone()); - } + // Check if bash triggers --allow-all-tools. This happens when: + // 1. Bash has an explicit wildcard entry (":*" or "*"), OR + // 2. Bash is not specified at all (None) — ado-aw agents always run in AWF sandbox, + // and gh-aw defaults to bash: ["*"] when sandbox is enabled (applyDefaultTools). + let bash_config = front_matter.tools.as_ref().and_then(|t| t.bash.as_ref()); + let use_allow_all_tools = match bash_config { + Some(cmds) if cmds.len() == 1 && (cmds[0] == ":*" || cmds[0] == "*") => true, + None => true, // default: all tools (matches gh-aw sandbox default) + _ => false, + }; // Edit tool: enabled by default, can be disabled with `edit: false` let edit_enabled = front_matter @@ -511,40 +480,72 @@ pub fn generate_copilot_params( .as_ref() .and_then(|t| t.edit) .unwrap_or(true); - if edit_enabled { - allowed_tools.push("write".to_string()); - } - // Bash tool: use configured list, or defaults if not specified - let mut bash_commands: Vec = - match front_matter.tools.as_ref().and_then(|t| t.bash.as_ref()) { - Some(cmds) if cmds.len() == 1 && cmds[0] == ":*" => { - // Unrestricted: single wildcard entry - allowed_tools.push("shell(:*)".to_string()); - vec![] - } - Some(cmds) if cmds.is_empty() => { - // Explicitly disabled: no bash commands - vec![] + // When --allow-all-tools is active, skip individual tool collection entirely. + // --allow-all-tools is a superset that permits all tool calls regardless. + let mut allowed_tools: Vec = Vec::new(); + + if !use_allow_all_tools { + // Collect tool permissions from extensions (github, safeoutputs, azure-devops, etc.) + for ext in extensions { + for tool in ext.allowed_copilot_tools() { + if !allowed_tools.contains(&tool) { + allowed_tools.push(tool); + } } - Some(cmds) => { - // Explicit list of commands - cmds.clone() + } + + // Collect tool permissions from user-defined MCP servers (sorted for deterministic output). + // Only add --allow-tool for MCPs that will actually produce an MCPG entry (i.e., + // WithOptions that have a container or url). McpConfig::Enabled(true) has no backing + // server in MCPG, so granting the permission would cause confusing runtime errors. + let mut sorted_mcps: Vec<_> = front_matter.mcp_servers.iter().collect(); + sorted_mcps.sort_by(|(a, _), (b, _)| a.cmp(b)); + for (name, config) in sorted_mcps { + // Skip servers already provided by extensions (case-insensitive to match + // generate_mcpg_config's eq_ignore_ascii_case guard for reserved names) + if allowed_tools.iter().any(|t| t.eq_ignore_ascii_case(name)) { + continue; } - None => { - // Default safe commands - DEFAULT_BASH_COMMANDS.iter().map(|s| (*s).to_string()).collect() + // Only add MCPs that have a backing server (container or url) + let has_backing_server = match config { + crate::compile::types::McpConfig::Enabled(_) => false, + crate::compile::types::McpConfig::WithOptions(opts) => { + opts.enabled.unwrap_or(true) + && (opts.container.is_some() || opts.url.is_some()) + } + }; + if !has_backing_server { + continue; } - }; + allowed_tools.push(name.clone()); + } - // Auto-add extension-declared bash commands (runtimes + first-party tools) - let is_unrestricted_bash = front_matter - .tools - .as_ref() - .and_then(|t| t.bash.as_ref()) - .is_some_and(|cmds| cmds.len() == 1 && cmds[0] == ":*"); + if edit_enabled { + allowed_tools.push("write".to_string()); + } + + // Bash tool: use the explicitly configured list. + // When bash is None (not specified), use_allow_all_tools is true and this + // block is skipped entirely (gh-aw sandbox default = wildcard). + let mut bash_commands: Vec = + match front_matter.tools.as_ref().and_then(|t| t.bash.as_ref()) { + Some(cmds) if cmds.is_empty() => { + // Explicitly disabled: no bash commands + vec![] + } + Some(cmds) => { + // Explicit list of commands + cmds.clone() + } + None => { + // Unreachable: bash=None → use_allow_all_tools=true → block skipped. + // Keep as defensive fallback. + vec![] + } + }; - if !is_unrestricted_bash { + // Auto-add extension-declared bash commands (runtimes + first-party tools) for ext in extensions { for cmd in ext.required_bash_commands() { if !bash_commands.contains(&cmd) { @@ -552,19 +553,19 @@ pub fn generate_copilot_params( } } } - } - for cmd in &bash_commands { - // Reject single quotes in bash commands — copilot_params are embedded inside - // a single-quoted bash string in the AWF command. - if cmd.contains('\'') { - anyhow::bail!( - "Bash command '{}' contains a single quote, which is not allowed \ - (would break AWF shell quoting).", - cmd - ); + for cmd in &bash_commands { + // Reject single quotes in bash commands — copilot_params are embedded inside + // a single-quoted bash string in the AWF command. + if cmd.contains('\'') { + anyhow::bail!( + "Bash command '{}' contains a single quote, which is not allowed \ + (would break AWF shell quoting).", + cmd + ); + } + allowed_tools.push(format!("shell({})", cmd)); } - allowed_tools.push(format!("shell({})", cmd)); } let mut params = Vec::new(); @@ -604,16 +605,26 @@ pub fn generate_copilot_params( params.push("--disable-builtin-mcps".to_string()); params.push("--no-ask-user".to_string()); - for tool in allowed_tools { - if tool.contains('(') || tool.contains(')') || tool.contains(' ') { - // Use double quotes - the copilot_params are embedded inside a single-quoted - // bash string in the AWF command, so single quotes would break quoting. - params.push(format!("--allow-tool \"{}\"", tool)); - } else { - params.push(format!("--allow-tool {}", tool)); + if use_allow_all_tools { + params.push("--allow-all-tools".to_string()); + } else { + for tool in allowed_tools { + if tool.contains('(') || tool.contains(')') || tool.contains(' ') { + // Use double quotes - the copilot_params are embedded inside a single-quoted + // bash string in the AWF command, so single quotes would break quoting. + params.push(format!("--allow-tool \"{}\"", tool)); + } else { + params.push(format!("--allow-tool {}", tool)); + } } } + // --allow-all-paths when edit is enabled — lets the agent write to any file path. + // Emitted independently of --allow-all-tools (matches gh-aw behavior). + if edit_enabled { + params.push("--allow-all-paths".to_string()); + } + Ok(params.join(" ")) } @@ -2378,7 +2389,22 @@ mod tests { azure_devops: None, }); let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap(); - assert!(params.contains("--allow-tool \"shell(:*)\"")); + assert!(params.contains("--allow-all-tools"), "wildcard bash should emit --allow-all-tools"); + assert!(!params.contains("--allow-tool"), "no individual --allow-tool flags with --allow-all-tools"); + } + + #[test] + fn test_copilot_params_bash_star_wildcard() { + let mut fm = minimal_front_matter(); + fm.tools = Some(crate::compile::types::ToolsConfig { + bash: Some(vec!["*".to_string()]), + edit: None, + cache_memory: None, + azure_devops: None, + }); + let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap(); + assert!(params.contains("--allow-all-tools"), "\"*\" should behave same as \":*\""); + assert!(!params.contains("--allow-tool"), "no individual --allow-tool flags with --allow-all-tools"); } #[test] @@ -2394,9 +2420,53 @@ mod tests { assert!(!params.contains("shell(")); } + #[test] + fn test_copilot_params_allow_all_paths_when_edit_enabled() { + let fm = minimal_front_matter(); // edit defaults to true, bash defaults to wildcard + let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap(); + assert!(params.contains("--allow-all-paths"), "edit enabled (default) should emit --allow-all-paths"); + assert!(params.contains("--allow-all-tools"), "default (no bash) should emit --allow-all-tools"); + assert!(!params.contains("--allow-tool"), "no individual --allow-tool flags with --allow-all-tools"); + } + + #[test] + fn test_copilot_params_no_allow_all_paths_when_edit_disabled() { + let mut fm = minimal_front_matter(); + fm.tools = Some(crate::compile::types::ToolsConfig { + bash: None, + edit: Some(false), + cache_memory: None, + azure_devops: None, + }); + let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap(); + assert!(!params.contains("--allow-all-paths"), "edit disabled should NOT emit --allow-all-paths"); + assert!(!params.contains("--allow-tool write"), "edit disabled should NOT emit --allow-tool write"); + } + + #[test] + fn test_copilot_params_allow_all_tools_with_allow_all_paths() { + let mut fm = minimal_front_matter(); + fm.tools = Some(crate::compile::types::ToolsConfig { + bash: Some(vec![":*".to_string()]), + edit: Some(true), + cache_memory: None, + azure_devops: None, + }); + let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap(); + assert!(params.contains("--allow-all-tools"), "wildcard bash should emit --allow-all-tools"); + assert!(params.contains("--allow-all-paths"), "edit enabled should still emit --allow-all-paths"); + assert!(!params.contains("--allow-tool"), "no individual --allow-tool flags"); + } + #[test] fn test_copilot_params_lean_adds_bash_commands() { let mut fm = minimal_front_matter(); + fm.tools = Some(crate::compile::types::ToolsConfig { + bash: Some(vec!["cat".to_string()]), + edit: None, + cache_memory: None, + azure_devops: None, + }); fm.runtimes = Some(crate::compile::types::RuntimesConfig { lean: Some(crate::runtimes::lean::LeanRuntimeConfig::Enabled(true)), }); @@ -2404,8 +2474,8 @@ mod tests { assert!(params.contains("shell(lean)"), "lean command should be allowed"); assert!(params.contains("shell(lake)"), "lake command should be allowed"); assert!(params.contains("shell(elan)"), "elan command should be allowed"); - // Default bash commands should still be present - assert!(params.contains("shell(cat)"), "default commands should remain"); + // Explicit bash commands should still be present + assert!(params.contains("shell(cat)"), "explicit commands should remain"); } #[test] @@ -2421,9 +2491,9 @@ mod tests { lean: Some(crate::runtimes::lean::LeanRuntimeConfig::Enabled(true)), }); let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap(); - assert!(params.contains("shell(:*)"), "unrestricted should be preserved"); - // Should NOT add individual lean commands when unrestricted - assert!(!params.contains("shell(lean)"), "lean not needed with :*"); + assert!(params.contains("--allow-all-tools"), "wildcard should use --allow-all-tools"); + // Should NOT add individual tool flags when --allow-all-tools is active + assert!(!params.contains("--allow-tool"), "no individual tool flags with --allow-all-tools"); } #[test] @@ -2443,6 +2513,12 @@ mod tests { #[test] fn test_copilot_params_allow_tool_for_container_mcp() { let mut fm = minimal_front_matter(); + fm.tools = Some(crate::compile::types::ToolsConfig { + bash: Some(vec!["cat".to_string()]), + edit: None, + cache_memory: None, + azure_devops: None, + }); fm.mcp_servers.insert( "my-tool".to_string(), McpConfig::WithOptions(McpOptions { @@ -2457,6 +2533,12 @@ mod tests { #[test] fn test_copilot_params_allow_tool_for_url_mcp() { let mut fm = minimal_front_matter(); + fm.tools = Some(crate::compile::types::ToolsConfig { + bash: Some(vec!["cat".to_string()]), + edit: None, + cache_memory: None, + azure_devops: None, + }); fm.mcp_servers.insert( "remote-ado".to_string(), McpConfig::WithOptions(McpOptions { @@ -2482,6 +2564,12 @@ mod tests { #[test] fn test_copilot_params_allow_tool_mcps_sorted() { let mut fm = minimal_front_matter(); + fm.tools = Some(crate::compile::types::ToolsConfig { + bash: Some(vec!["cat".to_string()]), + edit: None, + cache_memory: None, + azure_devops: None, + }); fm.mcp_servers.insert( "z-tool".to_string(), McpConfig::WithOptions(McpOptions { diff --git a/tests/compiler_tests.rs b/tests/compiler_tests.rs index 81eca3f1..d8eb0b07 100644 --- a/tests/compiler_tests.rs +++ b/tests/compiler_tests.rs @@ -2706,7 +2706,7 @@ network: /// Verifies that a pipeline compiled with `runtimes: lean: true` contains: /// - The elan installer step (`elan-init.sh`) /// - Lean ecosystem domains in the network allow-list (`elan.lean-lang.org`) -/// - Lean tool shell allow-args (`shell(lean)`, `shell(lake)`, `shell(elan)`) +/// - `--allow-all-tools` (default when bash is not explicitly configured) /// - No unreplaced `{{ }}` template markers #[test] fn test_lean_runtime_compiled_output() { @@ -2764,18 +2764,11 @@ Prove theorems and build Lean 4 projects. "Compiled output should include elan.lean-lang.org in allowed domains" ); - // Lean tools should appear as shell allow-args for the Copilot CLI + // With no explicit bash config, default is --allow-all-tools (gh-aw sandbox default). + // Lean tools are implicitly covered by --allow-all-tools. assert!( - compiled.contains("shell(lean)"), - "Compiled output should include shell(lean) in --allow-tool args" - ); - assert!( - compiled.contains("shell(lake)"), - "Compiled output should include shell(lake) in --allow-tool args" - ); - assert!( - compiled.contains("shell(elan)"), - "Compiled output should include shell(elan) in --allow-tool args" + compiled.contains("--allow-all-tools"), + "Compiled output should include --allow-all-tools (default when bash is not specified)" ); // Verify no unreplaced {{ markers }} remain From 9c83842c6aa3ce8d21dd2e3a41ef5fd3cef068ff Mon Sep 17 00:00:00 2001 From: James Devine Date: Tue, 21 Apr 2026 16:54:40 +0100 Subject: [PATCH 2/3] fix: address review feedback on tool allow list comments - Add comment noting wildcard+command mixing is unsupported (cmds.len()==1) - Add comment explaining why restricted-bash path emits both --allow-tool write and --allow-all-paths (tool identity vs path scope) - Replace silent vec![] fallback with debug_assert! in unreachable None arm Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/compile/common.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/compile/common.rs b/src/compile/common.rs index 9ec01dd5..77a43cc0 100644 --- a/src/compile/common.rs +++ b/src/compile/common.rs @@ -467,6 +467,10 @@ pub fn generate_copilot_params( // 1. Bash has an explicit wildcard entry (":*" or "*"), OR // 2. Bash is not specified at all (None) — ado-aw agents always run in AWF sandbox, // and gh-aw defaults to bash: ["*"] when sandbox is enabled (applyDefaultTools). + // + // Note: wildcard detection requires exactly one entry (cmds.len() == 1). Mixing a + // wildcard with other commands (e.g. bash: [":*", "cat"]) is not supported and will + // fall through to the restricted path, emitting "shell(:*)" literally. let bash_config = front_matter.tools.as_ref().and_then(|t| t.bash.as_ref()); let use_allow_all_tools = match bash_config { Some(cmds) if cmds.len() == 1 && (cmds[0] == ":*" || cmds[0] == "*") => true, @@ -521,6 +525,9 @@ pub fn generate_copilot_params( allowed_tools.push(name.clone()); } + // Intentional: with restricted bash, both --allow-tool write (tool identity) + // and --allow-all-paths (path scope) are emitted. --allow-all-tools subsumes + // --allow-tool write, so only --allow-all-paths is needed on that path. if edit_enabled { allowed_tools.push("write".to_string()); } @@ -539,8 +546,9 @@ pub fn generate_copilot_params( cmds.clone() } None => { - // Unreachable: bash=None → use_allow_all_tools=true → block skipped. - // Keep as defensive fallback. + // Invariant: bash=None → use_allow_all_tools=true → this block is + // skipped. Panic in debug builds if the invariant is ever broken. + debug_assert!(false, "bash=None should imply use_allow_all_tools=true"); vec![] } }; From 25f795b611c6372537b962859b31a37deffbf945 Mon Sep 17 00:00:00 2001 From: James Devine Date: Tue, 21 Apr 2026 17:06:30 +0100 Subject: [PATCH 3/3] fix: address remaining review feedback on tool allow lists - Update AGENTS.md copilot_params docs: --allow-all-tools now mentions bash-omitted default, --allow-tool references configured tools instead of deleted DEFAULT_BASH_COMMANDS list - Replace debug_assert!(false, ...) with unreachable!() for the bash=None invariant (idiomatic Rust for proven-unreachable paths) - Strengthen test_copilot_params_custom_mcp_no_mcp_flag assertion to check --allow-tool (not non-existent --mcp flag) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 4 ++-- src/compile/common.rs | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ad3d39af..e7f7bd14 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -634,8 +634,8 @@ Additional params provided to copilot CLI. The compiler generates: - `--model ` - AI model from `engine` front matter field (default: claude-opus-4.5) - `--no-ask-user` - Prevents interactive prompts - `--disable-builtin-mcps` - Disables all built-in Copilot CLI MCPs (single flag, no argument) -- `--allow-all-tools` - When bash has wildcard (`":*"` or `"*"`), allows all tools instead of individual `--allow-tool` flags -- `--allow-tool ` - When bash is NOT wildcard, explicitly allows specific tools (github, safeoutputs, write, shell commands like cat, date, echo, grep, head, ls, pwd, sort, tail, uniq, wc, yq) +- `--allow-all-tools` - When bash is omitted (default) or has a wildcard (`":*"` or `"*"`), allows all tools instead of individual `--allow-tool` flags +- `--allow-tool ` - When bash is NOT wildcard, explicitly allows configured tools (github, safeoutputs, write, and shell commands from the `bash:` field plus any runtime-required commands) - `--allow-all-paths` - When `edit` tool is enabled (default), allows the agent to write to any file path MCP servers are handled entirely by the MCP Gateway (MCPG) and are not passed as copilot CLI params. diff --git a/src/compile/common.rs b/src/compile/common.rs index 77a43cc0..f885f51f 100644 --- a/src/compile/common.rs +++ b/src/compile/common.rs @@ -547,9 +547,8 @@ pub fn generate_copilot_params( } None => { // Invariant: bash=None → use_allow_all_tools=true → this block is - // skipped. Panic in debug builds if the invariant is ever broken. - debug_assert!(false, "bash=None should imply use_allow_all_tools=true"); - vec![] + // skipped. Panic if the invariant is ever broken. + unreachable!("bash=None should imply use_allow_all_tools=true") } }; @@ -2515,7 +2514,10 @@ mod tests { }), ); let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap(); - assert!(!params.contains("--mcp my-tool")); + assert!( + !params.contains("--allow-tool my-tool"), + "default (all-tools) mode should not emit individual --allow-tool for MCPs" + ); } #[test]