From 313df2b229a4db2a2e531b48a618a421a7bc5ebb Mon Sep 17 00:00:00 2001 From: Joao Moura Date: Sat, 17 Oct 2026 23:35:05 -0700 Subject: [PATCH] fix(cli): pin AMP's proxy and CA in the JSON-crew runner too `crewai run` runs a JSON crew in a fresh `uv run python -c` interpreter that never imports cli.py, and `_run_json_crew` loads the project's .env with override=True before tracing first imports crewai_core.plus_api, so the pin recorded the project's proxy and CA. The runner now imports plus_api before anything else. Declarative flows re-run through `uv run crewai run`, which already pins in cli.py. Also keeps `_requests_sent_by` off machine and OS proxies, so a developer's proxy can't mount a real transport ahead of its MockTransport. Co-Authored-By: Claude Opus 5.5 --- lib/cli/src/crewai_cli/run_crew.py | 8 ++++++ lib/cli/tests/test_plus_api.py | 11 +++++-- lib/cli/tests/test_run_crew.py | 46 ++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/lib/cli/src/crewai_cli/run_crew.py b/lib/cli/src/crewai_cli/run_crew.py index e8ceb9677..1aa00465b 100644 --- a/lib/cli/src/crewai_cli/run_crew.py +++ b/lib/cli/src/crewai_cli/run_crew.py @@ -56,6 +56,14 @@ source_dir = os.environ.get("CREWAI_RUNNER_SOURCE_DIR") if source_dir: sys.path.insert(0, source_dir) +# This interpreter never imports cli.py, so pin the machine's proxy and CA for +# AMP requests here, before _run_json_crew loads the project's .env. A +# crewai-core too old to have plus_api pins nothing and ignores proxies anyway. +try: + import crewai_core.plus_api +except ImportError: + pass + package_dir = Path(os.environ["CREWAI_CLI_RUNNER_PACKAGE_DIR"]) package_spec = importlib.util.spec_from_file_location( "crewai_cli", diff --git a/lib/cli/tests/test_plus_api.py b/lib/cli/tests/test_plus_api.py index d9521882c..671b43312 100644 --- a/lib/cli/tests/test_plus_api.py +++ b/lib/cli/tests/test_plus_api.py @@ -551,9 +551,14 @@ def _requests_sent_by(call: Callable[[PlusAPI], object]) -> list[httpx.Request]: return httpx.Response(200, json={}) transport = httpx.MockTransport(answer) - with patch( - "crewai_core.plus_api.httpx.Client", - side_effect=lambda **kwargs: _RealHttpxClient(transport=transport, **kwargs), + # A machine or OS proxy would mount a real transport ahead of the mock one. + with ( + patch.dict("crewai_core.plus_api._MACHINE_TRANSPORT_ENV", clear=True), + patch("crewai_core.plus_api._os_proxies", dict), + patch( + "crewai_core.plus_api.httpx.Client", + side_effect=lambda **kwargs: _RealHttpxClient(transport=transport, **kwargs), + ), ): call(PlusAPI("test_api_key")) return sent diff --git a/lib/cli/tests/test_run_crew.py b/lib/cli/tests/test_run_crew.py index f13104289..df89758c5 100644 --- a/lib/cli/tests/test_run_crew.py +++ b/lib/cli/tests/test_run_crew.py @@ -1,5 +1,6 @@ """Tests for crewai_cli.run_crew JSON crew handling.""" +import json import os from pathlib import Path import subprocess @@ -232,6 +233,51 @@ def test_json_runner_code_loads_current_cli_package_over_project_env(tmp_path: P assert marker.read_text() == "current:trained.pkl" +def test_json_runner_pins_the_machines_proxy_before_the_project_env_loads( + tmp_path: Path, +): + """The runner is a fresh interpreter that never imports cli.py. It reads the + machine's proxy and CA for AMP before `_run_json_crew` loads the project's + .env, so that file cannot choose where the saved login travels.""" + from crewai_core import plus_api + + current_pkg = tmp_path / "cli" / "crewai_cli" + current_pkg.mkdir(parents=True) + pinned = tmp_path / "pinned.json" + (current_pkg / "__init__.py").write_text("") + (current_pkg / "run_crew.py").write_text( + "import json\n" + "from pathlib import Path\n" + "from dotenv import load_dotenv\n" + "def _run_json_crew(trained_agents_file=None):\n" + " load_dotenv(Path.cwd() / '.env', override=True)\n" + " from crewai_core import plus_api\n" + f" Path({str(pinned)!r}).write_text(json.dumps(plus_api._MACHINE_TRANSPORT_ENV))\n" + ) + (tmp_path / ".env").write_text( + "HTTPS_PROXY=http://project-proxy.test:8080\nSSL_CERT_FILE=/project/ca.pem\n" + ) + + env = { + key: value + for key, value in os.environ.items() + if not plus_api._sets_transport(key) + } + env["HTTPS_PROXY"] = "http://machine-proxy.test:3128" + env[run_crew_module._CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV] = str(current_pkg) + + subprocess.run( + [sys.executable, "-c", run_crew_module._JSON_CREW_RUNNER_CODE], + check=True, + env=env, + cwd=tmp_path, + ) + + assert json.loads(pinned.read_text()) == { + "HTTPS_PROXY": "http://machine-proxy.test:3128" + } + + def test_json_runner_imports_with_older_project_env_crewai_core(tmp_path: Path): old_parent = tmp_path / "old_env" old_crewai_core = old_parent / "crewai_core"