From a136c6821fa242715f24e27ab6467e7374fbad2c Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:25:53 -0500 Subject: [PATCH 1/3] fix(copilot): use --yolo to grant all permissions in non-interactive mode The Copilot CLI's --allow-all-tools flag only covers tool execution permissions but does not grant path or URL access. When the Copilot agent autonomously runs shell commands (e.g. npm run build) during workflow execution, the CLI blocks path access and cannot prompt for approval in non-interactive mode, producing: Permission denied and could not request permission from user Replace --allow-all-tools with --yolo (equivalent to --allow-all-tools --allow-all-paths --allow-all-urls) to grant all three permission types. Rename the opt-out env var from SPECKIT_ALLOW_ALL_TOOLS to SPECKIT_COPILOT_ALLOW_ALL to match the formal --allow-all alias and scope it to the Copilot integration. Fixes #2294 --- src/specify_cli/integrations/copilot/__init__.py | 15 ++++++++------- tests/test_workflows.py | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/specify_cli/integrations/copilot/__init__.py b/src/specify_cli/integrations/copilot/__init__.py index be7fc819f6..6f28ab5583 100644 --- a/src/specify_cli/integrations/copilot/__init__.py +++ b/src/specify_cli/integrations/copilot/__init__.py @@ -50,13 +50,14 @@ def build_exec_args( output_json: bool = True, ) -> list[str] | None: # GitHub Copilot CLI uses ``copilot -p "prompt"`` for - # non-interactive mode. --allow-all-tools is required for the - # agent to perform file edits and shell commands. Controlled - # by SPECKIT_ALLOW_ALL_TOOLS env var (default: enabled). + # non-interactive mode. --yolo enables all permissions + # (tools, paths, and URLs) so the agent can perform file + # edits and shell commands without interactive prompts. + # Controlled by SPECKIT_COPILOT_ALLOW_ALL env var (default: enabled). import os args = ["copilot", "-p", prompt] - if os.environ.get("SPECKIT_ALLOW_ALL_TOOLS", "1") != "0": - args.append("--allow-all-tools") + if os.environ.get("SPECKIT_COPILOT_ALLOW_ALL", "1") != "0": + args.append("--yolo") if model: args.extend(["--model", model]) if output_json: @@ -96,8 +97,8 @@ def dispatch_command( "copilot", "-p", prompt, "--agent", agent_name, ] - if os.environ.get("SPECKIT_ALLOW_ALL_TOOLS", "1") != "0": - cli_args.append("--allow-all-tools") + if os.environ.get("SPECKIT_COPILOT_ALLOW_ALL", "1") != "0": + cli_args.append("--yolo") if model: cli_args.extend(["--model", model]) if not stream: diff --git a/tests/test_workflows.py b/tests/test_workflows.py index c972945d04..a77f578284 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -373,7 +373,7 @@ def test_copilot_exec_args(self): args = impl.build_exec_args("do stuff", model="claude-sonnet-4-20250514") assert args[0] == "copilot" assert "-p" in args - assert "--allow-all-tools" in args + assert "--yolo" in args assert "--model" in args def test_ide_only_returns_none(self): From c42e6477a22280721f36594c3f121a709c0063ff Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:32:26 -0500 Subject: [PATCH 2/3] review: deprecate SPECKIT_ALLOW_ALL_TOOLS, rename to SPECKIT_COPILOT_ALLOW_ALL_TOOLS Address Copilot review feedback: - Honour the old SPECKIT_ALLOW_ALL_TOOLS env var as a fallback with a DeprecationWarning so existing opt-outs are not silently ignored. - Rename the new canonical env var to SPECKIT_COPILOT_ALLOW_ALL_TOOLS. - New var takes precedence when both are set. - Use monkeypatch in tests to avoid flakiness from ambient env vars. - Add tests for deprecation warning, precedence, and opt-out paths. --- .../integrations/copilot/__init__.py | 36 ++++++++++++++++--- tests/test_workflows.py | 32 ++++++++++++++++- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/specify_cli/integrations/copilot/__init__.py b/src/specify_cli/integrations/copilot/__init__.py index 6f28ab5583..b7749a717f 100644 --- a/src/specify_cli/integrations/copilot/__init__.py +++ b/src/specify_cli/integrations/copilot/__init__.py @@ -10,7 +10,9 @@ from __future__ import annotations import json +import os import shutil +import warnings from pathlib import Path from typing import Any @@ -18,6 +20,30 @@ from ..manifest import IntegrationManifest +def _allow_all() -> bool: + """Return True if the Copilot CLI should run with full permissions. + + Checks ``SPECKIT_COPILOT_ALLOW_ALL_TOOLS`` first (new canonical name). + Falls back to the deprecated ``SPECKIT_ALLOW_ALL_TOOLS`` if set, + emitting a deprecation warning. Default when neither is set: enabled. + """ + new_var = os.environ.get("SPECKIT_COPILOT_ALLOW_ALL_TOOLS") + if new_var is not None: + return new_var != "0" + + old_var = os.environ.get("SPECKIT_ALLOW_ALL_TOOLS") + if old_var is not None: + warnings.warn( + "SPECKIT_ALLOW_ALL_TOOLS is deprecated; " + "use SPECKIT_COPILOT_ALLOW_ALL_TOOLS instead.", + DeprecationWarning, + stacklevel=2, + ) + return old_var != "0" + + return True + + class CopilotIntegration(IntegrationBase): """Integration for GitHub Copilot (VS Code IDE + CLI). @@ -53,10 +79,11 @@ def build_exec_args( # non-interactive mode. --yolo enables all permissions # (tools, paths, and URLs) so the agent can perform file # edits and shell commands without interactive prompts. - # Controlled by SPECKIT_COPILOT_ALLOW_ALL env var (default: enabled). - import os + # Controlled by SPECKIT_COPILOT_ALLOW_ALL_TOOLS env var + # (default: enabled). The deprecated SPECKIT_ALLOW_ALL_TOOLS + # is also honoured as a fallback. args = ["copilot", "-p", prompt] - if os.environ.get("SPECKIT_COPILOT_ALLOW_ALL", "1") != "0": + if _allow_all(): args.append("--yolo") if model: args.extend(["--model", model]) @@ -92,12 +119,11 @@ def dispatch_command( agent_name = f"speckit.{stem}" prompt = args or "" - import os cli_args = [ "copilot", "-p", prompt, "--agent", agent_name, ] - if os.environ.get("SPECKIT_COPILOT_ALLOW_ALL", "1") != "0": + if _allow_all(): cli_args.append("--yolo") if model: cli_args.extend(["--model", model]) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index a77f578284..000a2698c1 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -367,7 +367,9 @@ def test_codex_exec_args(self): assert args[2] == "do stuff" assert "--json" in args - def test_copilot_exec_args(self): + def test_copilot_exec_args(self, monkeypatch): + monkeypatch.delenv("SPECKIT_COPILOT_ALLOW_ALL_TOOLS", raising=False) + monkeypatch.delenv("SPECKIT_ALLOW_ALL_TOOLS", raising=False) from specify_cli.integrations.copilot import CopilotIntegration impl = CopilotIntegration() args = impl.build_exec_args("do stuff", model="claude-sonnet-4-20250514") @@ -376,6 +378,34 @@ def test_copilot_exec_args(self): assert "--yolo" in args assert "--model" in args + def test_copilot_new_env_var_disables_yolo(self, monkeypatch): + monkeypatch.setenv("SPECKIT_COPILOT_ALLOW_ALL_TOOLS", "0") + monkeypatch.delenv("SPECKIT_ALLOW_ALL_TOOLS", raising=False) + from specify_cli.integrations.copilot import CopilotIntegration + impl = CopilotIntegration() + args = impl.build_exec_args("do stuff") + assert "--yolo" not in args + + def test_copilot_deprecated_env_var_still_honoured(self, monkeypatch): + monkeypatch.delenv("SPECKIT_COPILOT_ALLOW_ALL_TOOLS", raising=False) + monkeypatch.setenv("SPECKIT_ALLOW_ALL_TOOLS", "0") + import warnings + from specify_cli.integrations.copilot import CopilotIntegration + impl = CopilotIntegration() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + args = impl.build_exec_args("do stuff") + assert "--yolo" not in args + assert any("SPECKIT_ALLOW_ALL_TOOLS is deprecated" in str(x.message) for x in w) + + def test_copilot_new_env_var_takes_precedence(self, monkeypatch): + monkeypatch.setenv("SPECKIT_COPILOT_ALLOW_ALL_TOOLS", "1") + monkeypatch.setenv("SPECKIT_ALLOW_ALL_TOOLS", "0") + from specify_cli.integrations.copilot import CopilotIntegration + impl = CopilotIntegration() + args = impl.build_exec_args("do stuff") + assert "--yolo" in args + def test_ide_only_returns_none(self): from specify_cli.integrations.windsurf import WindsurfIntegration impl = WindsurfIntegration() From 38791a213ea4fed38edbfa47348d472f5142dbd8 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:36:56 -0500 Subject: [PATCH 3/3] review: use UserWarning instead of DeprecationWarning DeprecationWarning is suppressed by default in Python, so users relying on the old SPECKIT_ALLOW_ALL_TOOLS env var would never see the deprecation notice during normal CLI runs. Switch to UserWarning which is always shown. Update test to also assert the warning category. --- src/specify_cli/integrations/copilot/__init__.py | 2 +- tests/test_workflows.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/integrations/copilot/__init__.py b/src/specify_cli/integrations/copilot/__init__.py index b7749a717f..45ec6f3532 100644 --- a/src/specify_cli/integrations/copilot/__init__.py +++ b/src/specify_cli/integrations/copilot/__init__.py @@ -36,7 +36,7 @@ def _allow_all() -> bool: warnings.warn( "SPECKIT_ALLOW_ALL_TOOLS is deprecated; " "use SPECKIT_COPILOT_ALLOW_ALL_TOOLS instead.", - DeprecationWarning, + UserWarning, stacklevel=2, ) return old_var != "0" diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 000a2698c1..82bcf27eb1 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -396,7 +396,11 @@ def test_copilot_deprecated_env_var_still_honoured(self, monkeypatch): warnings.simplefilter("always") args = impl.build_exec_args("do stuff") assert "--yolo" not in args - assert any("SPECKIT_ALLOW_ALL_TOOLS is deprecated" in str(x.message) for x in w) + assert any( + "SPECKIT_ALLOW_ALL_TOOLS is deprecated" in str(x.message) + and issubclass(x.category, UserWarning) + for x in w + ) def test_copilot_new_env_var_takes_precedence(self, monkeypatch): monkeypatch.setenv("SPECKIT_COPILOT_ALLOW_ALL_TOOLS", "1")