From 14a25d80ce75d3866403b0d1b9952885e21839ea Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Wed, 22 Jul 2026 10:58:15 -0400 Subject: [PATCH] fix(runtime): resolve codex .cmd shim path so Windows spawns don't fail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, npm installs the codex CLI only as a .cmd shim, and create_subprocess_exec (CreateProcess) does no PATHEXT resolution — _run_codex_cli_with_stdin spawned the bare "codex" name and failed with FileNotFoundError ([WinError 2]) on every codex harness call. Each schema attempt then surfaced as "Schema retry N provider error: [WinError 2]" and reasoners fell back to their advisor-failure defaults in ~500ms. Route cmd[0] through the AgentField SDK's resolve_cli_command, which resolves the shim's real path via shutil.which and is a no-op on POSIX. Co-Authored-By: Claude Fable 5 --- swe_af/runtime/codex_harness_patch.py | 10 ++++++- tests/test_codex_harness_patch.py | 43 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/swe_af/runtime/codex_harness_patch.py b/swe_af/runtime/codex_harness_patch.py index 96eaa3fb..79ddbf35 100644 --- a/swe_af/runtime/codex_harness_patch.py +++ b/swe_af/runtime/codex_harness_patch.py @@ -88,8 +88,16 @@ async def _run_codex_cli_with_stdin( env: dict[str, str] | None, cwd: str | None, ) -> tuple[str, str, int]: + # Windows: CreateProcess does no PATHEXT resolution, and npm installs the + # codex CLI only as a .cmd shim — spawning the bare name raises + # FileNotFoundError ([WinError 2]) on every call even though the shell + # finds it. resolve_cli_command resolves the shim's real path via + # shutil.which (a no-op on POSIX and for names that already carry a path). + from agentfield.harness._cli import resolve_cli_command + proc = await asyncio.create_subprocess_exec( - *cmd, + resolve_cli_command(cmd[0]), + *cmd[1:], stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, diff --git a/tests/test_codex_harness_patch.py b/tests/test_codex_harness_patch.py index 3919e977..405b69e2 100644 --- a/tests/test_codex_harness_patch.py +++ b/tests/test_codex_harness_patch.py @@ -108,3 +108,46 @@ def test_non_codex_prompt_suffix_keeps_agentfield_write_tool_default(tmp_path) - assert "Write tool" in suffix assert "Codex CLI" not in suffix + + +def test_run_codex_cli_spawns_via_resolved_command(monkeypatch) -> None: + """The spawn must route cmd[0] through the SDK's resolve_cli_command. + + On Windows, npm installs the codex CLI only as a .cmd shim, and + CreateProcess does no PATHEXT resolution — spawning the bare name fails + with FileNotFoundError ([WinError 2]) on every codex harness call. + resolve_cli_command resolves the shim's real path (no-op on POSIX). + """ + import asyncio + + import agentfield.harness._cli as sdk_cli + + from swe_af.runtime.codex_harness_patch import _run_codex_cli_with_stdin + + spawned: dict = {} + + class FakeProc: + returncode = 0 + + async def communicate(self, data: bytes) -> tuple[bytes, bytes]: + spawned["stdin"] = data + return b"", b"" + + async def fake_exec(program: str, *args: str, **kwargs: object) -> FakeProc: + spawned["program"] = program + spawned["args"] = args + return FakeProc() + + monkeypatch.setattr(sdk_cli, "resolve_cli_command", lambda name: f"resolved::{name}") + monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_exec) + + stdout, stderr, returncode = asyncio.run( + _run_codex_cli_with_stdin( + ["codex", "exec", "--json"], "prompt", env=None, cwd=None + ) + ) + + assert spawned["program"] == "resolved::codex" + assert spawned["args"][:2] == ("exec", "--json") + assert spawned["stdin"] == b"prompt" + assert (stdout, stderr, returncode) == ("", "", 0)