Repository navigation
fix(agent): read the interactive PATH on Windows instead of a frozen login shell - #880
Conversation
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: make refresh and probe command/path handling consume the Windows environment correctly.
I reviewed the full github/main...HEAD diff, the probe and spawn callers, the refresh RPC paths, relevant history, backward compatibility, test changes, AGENTS.md, CONTEXT-MAP.md, and the Runtime architecture terms. The new tests add coverage rather than weakening existing assertions, but they miss the three end-to-end seams marked inline.
Verification: uv sync --extra dev; uv run pytest tests/test_subagent_host_env.py tests/test_subagent_probe.py tests/test_subagent_third_party.py -x (355 passed); git diff --check github/main...HEAD (passed).
…login shell The WebUI reported an installed subagent "not on the login shell PATH" while its interpreter sat on disk. Two Windows-only defects stacked: shlex.split defaults to POSIX, so the agent's launch command "C:\...\python.exe -m raven acp" had every backslash read as an escape and reached shutil.which as the bare relative name "C:Users...python.exe", which can only miss a PATH lookup. On win32 the command is tokenized posix=False, the way CommandLineToArgvW parses a real spawn, so the absolute interpreter is handed to which whole. The git-bash .exe and backslash spellings of SHELL are also recognized as drivable. Windows has no login shell to run "bash -lic env -0" against, so login_shell_env fell back to the gateway's frozen os.environ and an agent installed after boot was invisible forever. _capture_windows now reads the HKLM and HKCU Environment stores a fresh terminal is assembled from, recursively expands %VAR% references, and unions the store PATH with the live process PATH (a service's own additions like MinGit's usr/bin survive). Everything else is kept from os.environ so a child loses no proxy, temp or conda variables.
The probe alone split a command with posix=False on Windows, while
AcpClient.launch and the cli backend kept POSIX shlex.split. The two
then disagreed in both directions: the probe found the interpreter of
an unquoted {PYTHON} command that the launch still mangled into
C:Users...python.exe, and it kept the quotes shlex.quote puts round
host_raven_acp_command's path, so it reported missing a program the
launch starts.
One parser for the probe and both launchers is what #864 adds, so the
probe goes back to the split the launchers use rather than gaining a
second Windows rule here.
Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
The Windows capture stored its PATH under the registry's spelling, Path, while every reader asks for PATH: CPython keeps os.environ names upper-case on Windows, which is what the fallback this replaced had handed them. The probe read an empty PATH, shutil.which(exe, path="") answers None, and every bare-name agent read as missing, including the ones found on the gateway's PATH before the capture existed. It also copied every other stored value over raven's own. Those are what Windows builds an environment from, not what it hands a process: ComSpec and TEMP arrive unexpanded (%SystemRoot%\system32\cmd.exe, %USERPROFILE%\...), and the machine store's USERNAME is SYSTEM until a logon replaces it. A child given that ComSpec cannot run Popen(shell=True), which uses it only when it is an absolute path. The capture is now dict(os.environ) with PATH rebuilt from the stored machine and user Path followed by the live PATH. A stored reference expands with raven's own values first, since they are this logon's and the child carries them, then user over machine for a variable an installer added after raven started. The tests take Windows' shape: the stock store values unexpanded, an upper-case os.environ, and the ambient environment cleared, so a host that holds HTTP_PROXY beside http_proxy no longer fails them. The capture is checked through probe_all, its reader. Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
Check again, and every Connect or Test, refresh the capture through refresh_login_shell_env, which called the login-shell capture on every host. On Windows it found no shell, kept the first capture, and an agent installed after that stayed missing until a restart. With Git for Windows' SHELL it was worse: that bash is exported as C:\Program Files\Git\usr\bin\bash.exe, which _login_shell had been taught to drive, so a refresh ran bash -lic "env -0" from the POSIX bootstrap base and swapped an MSYS environment in, a ':'-joined PATH of /c/... entries with no SystemRoot. The first capture and every refresh now go through _capture_now, which reads the registry on Windows and the login shell elsewhere. The git-bash spelling is no longer recognized: _capture is not reachable on Windows any more, and a POSIX host never sees that spelling. The test fixture that pins the POSIX capture pins the same switch. Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
CreateProcess looks a bare program name up on the PATH of the process that calls it and never on the env block it is handed; POSIX exec reads the PATH of that env. So on Windows an agent installed after the gateway started, which the refreshed capture and the probe now find, still would not start: the launch searched the gateway's frozen PATH. resolve_program looks the name up on the child's PATH instead, by CreateProcess's own rule that a name with no extension means .exe, and puts in only what CreateProcess runs natively. A .cmd or .bat shim is left alone: CreateProcess never turns a bare name into one, and doing it here would put the shim's arguments, a cli agent's prompt among them, through cmd.exe's parser. The acp launch, the cli launch and the Kimi Code ask all start their program this way; POSIX is unchanged. Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
8388258 to
e79c65f
Compare
|
Round two is pushed as e79c65f. Two things the threads do not carry:
Command-line splitting is left to #864; the description says where the two meet. |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
I reviewed the full github/main...HEAD diff and the delta from my prior round, including the capture/refresh and probe/spawn callers, the new ACP/CLI/Kimi launch lookup, relevant history, backward compatibility, test changes, AGENTS.md, CLAUDE.md, CONTEXT-MAP.md, and the Runtime architecture terms. The three prior findings are fixed or correctly withdrawn, their tests cover the previously missing seams, and I found no newly introduced plain error. The explicitly documented Windows command-tokenization and .cmd work remains with #864 and is not a blocker for this scoped improvement.
Verification: uv sync --extra dev; uv run pytest tests/test_subagent_host_env.py tests/test_subagent_probe.py tests/test_subagent_third_party.py tests/test_subagent_kimi_code.py tests/test_rpc_subagents.py tests/test_cli_agents_commands.py -q (621 passed); git diff --check github/main...HEAD (passed); commit-message non-ASCII scan (passed). All three threads I opened in the prior round have been replied to and resolved.
|
Not a blocker -- notes from the acceptance pass on head e79c65f (merged onto tip 6d509a8). Nothing here holds the merge; each is small and can land as a follow-up. Severity of this one finding, not a verdict on the pull request. The blocking findings from this pass are review threads on the changed files; GitHub renders those collapsed, as a file name with no text.
Full suite on the merged tree: 27698 passed, 115 skipped, 7 failed -- the same 7 that fail on base in this environment with no PR code present. |
Summary
On Windows the sub-agents page and the launches read the environment the gateway started with, so an agent installed after it started stayed "not on the login shell PATH" until a restart. This reads the PATH a freshly opened terminal would get from the registry, takes it again on Check again, Connect and Test, and starts a bare program from it.
login_shell_envandrefresh_login_shell_envboth go through_capture_now: the registry on Windows, the login shell elsewhere. A refresh on Windows used to look for a login shell, find none, and keep the first capture._capture_windowsreturns raven's own environment with onlyPATHrebuilt: the machine and user stores'Path,%VAR%references expanded, followed by the live PATH. The stores' other values are not copied over raven's: they are what Windows builds an environment from (ComSpecandTEMPunexpanded, the machine'sUSERNAME=SYSTEM).os.environhas on Windows, which is the spellingprobe._login_pathand the other readers ask for, so an agent already on the gateway's PATH stays found beside the ones the stores add.resolve_program: CreateProcess looks a bare name up on the gateway's own PATH and never on the env block it is handed, so on Windows the acp launch, the cli launch and the Kimi Code ask look it up on the child's PATH instead, by CreateProcess's own rule that a name with no extension means.exe. Only an.exeor.comis put in: CreateProcess never turns a bare name into a.cmdor.bat, and doing it here would put a cli prompt through cmd.exe's parser.SHELLcan name there: its MSYS environment carries a:-joined POSIX PATH and noSystemRoot.Left out on purpose: splitting the command line. The probe keeps splitting a command the way both launchers do; a Windows rule for the probe alone (
posix=False) keeps the quotesshlex.quoteand a Program Files path carry and disagrees with the launch. #864 routes the probe and the launchers through one parser (raven/utils/commands.py), so the backslash mangling of a{PYTHON}command is closed there. Once both land, #864'slaunch_argv(a PATHEXT lookup on the gateway's PATH) andresolve_programhere want to become one lookup on the child's PATH; whichever lands second folds them.Type
Verification
python -m pytest tests/test_subagent_host_env.py tests/test_subagent_probe.py tests/test_subagent_third_party.py tests/test_subagent_kimi_code.py tests/test_rpc_subagents.py tests/test_cli_agents_commands.py -q(project venv, all extras, this tree onPYTHONPATH): 620 passed, 1 skipped.Full suite,
python -m pytest -qthe same way: 27683 passed, 119 skipped, 8 failed. Seven fail on this host at main too: fivetest_config_update_providers.pyproxy cases that read the host's proxy variables,test_subagent_node_runtime.py::test_what_cannot_be_read_names_nothingunder root, andtest_install_script.py::test_resolve_node_dir_answers_each_case_it_exists_forwith an npm on PATH.The eighth,
test_subagent_kimi_code.py::test_a_kimi_that_does_not_know_acp_is_told_to_upgrade, is a load race at main as well: with the stand-in forced to exit before the host's first_send, main and this branch both answer "stdin is closed" with no remedy.Mutation check: 13 single-construct mutants (refresh routing, the PATH spelling, the expansion order, the overlay, the union with the live PATH, folding store names, matching references without case, each of the three launch call sites, the
.exerule, the.cmdrefusal, the first capture's platform switch), each caught by the test written for it.Windows is simulated on Linux: a fake
winregholding the stock store values (REG_EXPAND_SZ unexpanded),os.environwith upper-case names, and;as the path separator for the probe test. Not run on a Windows host.ruff checkandruff format --checkover CI's targets,lint-imports(10 kept, 0 broken),ty checkon the touched modules, andscripts/check_large_files.pyandscripts/check_source_language.pyover the change: all clean.Relevant tests pass locally
Relevant lint / type checks pass locally
User-facing docs or screenshots are updated when needed
Risk
Windows only. On POSIX
_capture_nowis_captureandresolve_programreturns the argv it was given, so behavior matches main. On Windows a child gets raven's environment with PATH rebuilt registry-first, and a bare program found as an.exeor.comon that PATH starts from there; anything else is left to CreateProcess's own search, as before. Not changed here: a bare name that exists only as a.cmdshim (npx and other npm installs) still does not start on Windows, and a%PATH%reference inside a stored Path stays unexpanded. Rollback: revert the squash commit.Related Issues
#864