fix(copilot): use --yolo to grant all permissions in non-interactive mode (#2298)

* 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

* 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.

* 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.
This commit is contained in:
Manfred Riem
2026-04-21 11:41:52 -05:00
committed by GitHub
parent 52afbea47a
commit 370b5b4890
2 changed files with 72 additions and 11 deletions

View File

@@ -10,7 +10,9 @@ Copilot has several unique behaviors compared to standard markdown agents:
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 ..base import IntegrationBase
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.",
UserWarning,
stacklevel=2,
)
return old_var != "0"
return True
class CopilotIntegration(IntegrationBase):
"""Integration for GitHub Copilot (VS Code IDE + CLI).
@@ -50,13 +76,15 @@ class CopilotIntegration(IntegrationBase):
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).
import os
# 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_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_ALLOW_ALL_TOOLS", "1") != "0":
args.append("--allow-all-tools")
if _allow_all():
args.append("--yolo")
if model:
args.extend(["--model", model])
if output_json:
@@ -91,13 +119,12 @@ class CopilotIntegration(IntegrationBase):
agent_name = f"speckit.{stem}"
prompt = args or ""
import os
cli_args = [
"copilot", "-p", prompt,
"--agent", agent_name,
]
if os.environ.get("SPECKIT_ALLOW_ALL_TOOLS", "1") != "0":
cli_args.append("--allow-all-tools")
if _allow_all():
cli_args.append("--yolo")
if model:
cli_args.extend(["--model", model])
if not stream:

View File

@@ -367,15 +367,49 @@ class TestBuildExecArgs:
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")
assert args[0] == "copilot"
assert "-p" in args
assert "--allow-all-tools" in args
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)
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")
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()