From 31c399956ee5f88bdc0b188f77773883c46b4a93 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 4 Sep 2026 09:27:41 +0000 Subject: [PATCH] python: remove implicit PATH fallback for CLI executable resolution The runtime start path fell back to shutil.which(cli_path) whenever the resolved cli_path did not exist as a file. This let the SDK implicitly select an arbitrary same-named executable found on the host PATH instead of failing when the SDK-managed runtime or an explicitly configured path was not actually present, re-introducing the PATH-search behavior originally added by 05dd60e that #2524 asks to remove. Resolution order is unchanged (explicit path > COPILOT_CLI_PATH > SDK-managed downloaded runtime, see _resolve_runtime_entrypoint); only the undocumented PATH-search fallback at process start is removed. A missing resolved path now raises a RuntimeError up front instead of silently searching PATH. Adds a regression test asserting that a same-named executable placed on PATH is not used when the configured path does not exist. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/copilot/client.py | 13 ++++++++----- python/test_client.py | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/python/copilot/client.py b/python/copilot/client.py index ab3410a56c..e7026cab32 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -20,7 +20,6 @@ import logging import os import re -import shutil import subprocess import sys import threading @@ -4328,11 +4327,15 @@ async def _start_cli_server(self) -> None: cli_path = conn.path assert cli_path is not None # resolved in __init__ - # Verify CLI exists + # Verify the resolved CLI path exists. `cli_path` must already come from + # an explicit override, COPILOT_CLI_PATH, or the SDK-managed downloaded + # runtime (see `_resolve_runtime_entrypoint`); the SDK never falls back to + # searching PATH for an arbitrary system installation. if not os.path.exists(cli_path): - original_path = cli_path - if (cli_path := shutil.which(cli_path)) is None: - raise RuntimeError(f"Copilot CLI not found at {original_path}") + raise RuntimeError( + f"Copilot CLI not found at {cli_path!r}. Set an explicit path, " + "COPILOT_CLI_PATH, or ensure the SDK-managed runtime download succeeded." + ) # Start with user-provided args, then add SDK-managed args args = list(conn.args) + [ diff --git a/python/test_client.py b/python/test_client.py index e62154e247..fff506ec99 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -80,6 +80,25 @@ def test_copilot_cli_path_does_not_require_runtime_bundle(tmp_path): assert connection.path == str(explicit) +@pytest.mark.asyncio +async def test_missing_cli_path_does_not_fall_back_to_path_search(tmp_path): + """A missing resolved CLI path must fail rather than silently resolving an + arbitrary same-named executable found on PATH (see #2524 / commit 05dd60e).""" + path_dir = tmp_path / "on_path" + path_dir.mkdir() + decoy = path_dir / "copilot" + decoy.write_text("#!/bin/sh\necho decoy\n") + decoy.chmod(0o755) + + missing = tmp_path / "copilot" + connection = RuntimeConnection.for_stdio(path=str(missing)) + client = CopilotClient(connection=connection, env={"PATH": str(path_dir)}) + + with patch.dict(os.environ, {"PATH": str(path_dir)}): + with pytest.raises(RuntimeError, match="Copilot CLI not found"): + await client._start_cli_server() + + class TestBuiltinPluginDirectories: @staticmethod async def _start_client(paths=None):