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. |
…864) ## Summary Stored Windows launch commands were parsed with POSIX rules, which removed backslashes from unquoted interpreter and launcher paths. Launch consumers now share platform-aware parsing and quoting, while retaining compatibility with single-quoted absolute paths saved by older callers. - Read legacy single-quoted drive, UNC, and absolute POSIX path tokens on Windows, including path values after `=` and the quote concatenation emitted for apostrophes inside a path. Decode only that token so neighboring native Windows arguments keep their backslashes and literal apostrophes. Launch and readiness use the same compatibility decoder. - Preserve quoted paths in discovery and stale-row checks. Escaped quotes, CR/LF separators on POSIX, and arguments made only of backslashes no longer hide or merge a launcher path. - Share substitution across discovery, registration, scaffold smoke checks, generated installers, and all five shipped installers. Older Raven versions without the resolver refuse unsupported spaced command paths before registration, while keeping plain paths and separately passed working directories supported. - Quote Raven's own ACP executable and forwarded config path for the launch platform. Resolve bare programs on the child's PATH in directory order, preferring executables within each directory. ACP may resolve configured batch shims; prompt-bearing CLI and Kimi calls retain their lookup restriction. ## Type - [x] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification Checked on native Windows using an existing uv environment, with the source imported from the reviewed checkout and `PYTHONUTF8=1`. ```text uv run --no-sync python -m pytest tests/test_utils_commands.py tests/test_subagent_vendored_agents.py tests/test_subagent_builtin_agents.py tests/test_subagent_registry.py tests/test_subagent_third_party.py::test_cli_backend_preserves_stored_windows_path_quotes tests/test_subagent_host_env.py::test_acp_launch_preserves_stored_windows_path_quotes tests/test_subagent_probe.py::test_windows_probes_find_a_legacy_single_quoted_program -q -x -n 0 ``` Result: 292 passed. This covers a real CLI child launched through the backend, ACP spawn arguments, CLI/ACP availability probes, and existing/missing launcher checks, with both legacy and native quoting controls. The final 25 added cases were also run in an isolated process with the previously published parser: 12 failed and 13 passed. Four isolated mutations were caught: disabling compatibility (8 failures), removing the launch quote guard (1), removing the readiness quote guard (1), and using POSIX parsing for the entire Windows command (15). A seeded 50,000-case check through the native producer and both consumers reported no argv or readiness-token mismatches. Whole-branch checks: ```powershell $prFiles = @(git diff --name-only origin/main...HEAD -- '*.py') uv run --no-sync ruff check @prFiles uv run --no-sync ruff format --check @prFiles $production = @($prFiles | Where-Object { $_ -notlike 'tests/*' }) uv run --no-sync ty check @production uv run --no-sync lint-imports uv run --no-sync pre-commit run --from-ref origin/main --to-ref HEAD uv run --no-sync python scripts/check_commit_messages.py origin/main..HEAD uv run --no-sync python scripts/check_source_language.py origin/main...HEAD uv run --no-sync python scripts/check_large_files.py origin/main...HEAD git diff --check origin/main...HEAD ``` All passed: 24 Python files passed Ruff checks; production type checks passed; all 10 import contracts were kept. The pinned commitlint CLI also passed over the full branch range. The final documentation-only follow-up changes no executable behavior. Limits: a broader Windows selection stopped after 283 passing tests at `test_cli_probe_reports_the_resolved_absolute_path`, whose bare POSIX shell fixture is not resolved as a Windows executable. This is not counted as a passing suite. The ACP regression checks the arguments delivered to process creation; it does not claim to verify the existing POSIX-only process-group shutdown path on Windows. The local Docker daemon was unavailable, so no new local Linux or macOS run is claimed. The full repository suite and a live WebUI dispatch were not run locally for this revision. The pre-submit sweep covered the complete branch diff, all command producers and consumers, native/legacy quoting boundaries, installer fallbacks, failure paths, test strength, layer contracts, repository gates, and these disclosed verification limits. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed No screenshot update applies. Parser docstrings describe the legacy-path compatibility and how to preserve literal quotes. ## Risk - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes Compatibility applies to single-quoted absolute path tokens, including path values after `=`; it does not enable general POSIX shell syntax on Windows. Non-path apostrophes remain literal. To preserve literal single quotes around path-looking data, quote the entire argument with native double quotes. Existing stored path tokens are read compatibly without rewriting configuration; newly generated commands keep native quoting. POSIX launch parsing is unchanged. Readiness preserves foreign absolute-path spellings and checks launcher existence; it is not a general command validator. Older installers reject paths their Raven version cannot represent before changing the roster. ACP batch shims execute configured server arguments through cmd.exe. Prompt-bearing CLI and Kimi calls do not opt into batch lookup. A CLI agent installed only as an npm shim still needs an explicit executable path. Reverting the squash commit restores the prior behavior. ## Related Issues #880 --------- Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com>
…login shell (#880) ## 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_env` and `refresh_login_shell_env` both 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_windows` returns raven's own environment with only `PATH` rebuilt: 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 (`ComSpec` and `TEMP` unexpanded, the machine's `USERNAME=SYSTEM`). - The capture keeps the upper-case names `os.environ` has on Windows, which is the spelling `probe._login_path` and the other readers ask for, so an agent already on the gateway's PATH stays found beside the ones the stores add. - A stored reference expands with raven's own values first (they are this logon's, and the child carries them), then user over machine for a variable an installer added after raven started. - `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 `.exe` or `.com` is put in: CreateProcess never turns a bare name into a `.cmd` or `.bat`, and doing it here would put a cli prompt through cmd.exe's parser. - On Windows no shell is driven, including the Git for Windows bash that `SHELL` can name there: its MSYS environment carries a `:`-joined POSIX PATH and no `SystemRoot`. 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 quotes `shlex.quote` and 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's `launch_argv` (a PATHEXT lookup on the gateway's PATH) and `resolve_program` here want to become one lookup on the child's PATH; whichever lands second folds them. ## Type - [x] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## 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 on `PYTHONPATH`): 620 passed, 1 skipped. - Full suite, `python -m pytest -q` the same way: 27683 passed, 119 skipped, 8 failed. Seven fail on this host at main too: five `test_config_update_providers.py` proxy cases that read the host's proxy variables, `test_subagent_node_runtime.py::test_what_cannot_be_read_names_nothing` under root, and `test_install_script.py::test_resolve_node_dir_answers_each_case_it_exists_for` with 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 `.exe` rule, the `.cmd` refusal, the first capture's platform switch), each caught by the test written for it. - Windows is simulated on Linux: a fake `winreg` holding the stock store values (REG_EXPAND_SZ unexpanded), `os.environ` with upper-case names, and `;` as the path separator for the probe test. Not run on a Windows host. - `ruff check` and `ruff format --check` over CI's targets, `lint-imports` (10 kept, 0 broken), `ty check` on the touched modules, and `scripts/check_large_files.py` and `scripts/check_source_language.py` over the change: all clean. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk Windows only. On POSIX `_capture_now` is `_capture` and `resolve_program` returns 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 `.exe` or `.com` on 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 `.cmd` shim (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. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues #864 --------- Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
…864) ## Summary Stored Windows launch commands were parsed with POSIX rules, which removed backslashes from unquoted interpreter and launcher paths. Launch consumers now share platform-aware parsing and quoting, while retaining compatibility with single-quoted absolute paths saved by older callers. - Read legacy single-quoted drive, UNC, and absolute POSIX path tokens on Windows, including path values after `=` and the quote concatenation emitted for apostrophes inside a path. Decode only that token so neighboring native Windows arguments keep their backslashes and literal apostrophes. Launch and readiness use the same compatibility decoder. - Preserve quoted paths in discovery and stale-row checks. Escaped quotes, CR/LF separators on POSIX, and arguments made only of backslashes no longer hide or merge a launcher path. - Share substitution across discovery, registration, scaffold smoke checks, generated installers, and all five shipped installers. Older Raven versions without the resolver refuse unsupported spaced command paths before registration, while keeping plain paths and separately passed working directories supported. - Quote Raven's own ACP executable and forwarded config path for the launch platform. Resolve bare programs on the child's PATH in directory order, preferring executables within each directory. ACP may resolve configured batch shims; prompt-bearing CLI and Kimi calls retain their lookup restriction. ## Type - [x] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification Checked on native Windows using an existing uv environment, with the source imported from the reviewed checkout and `PYTHONUTF8=1`. ```text uv run --no-sync python -m pytest tests/test_utils_commands.py tests/test_subagent_vendored_agents.py tests/test_subagent_builtin_agents.py tests/test_subagent_registry.py tests/test_subagent_third_party.py::test_cli_backend_preserves_stored_windows_path_quotes tests/test_subagent_host_env.py::test_acp_launch_preserves_stored_windows_path_quotes tests/test_subagent_probe.py::test_windows_probes_find_a_legacy_single_quoted_program -q -x -n 0 ``` Result: 292 passed. This covers a real CLI child launched through the backend, ACP spawn arguments, CLI/ACP availability probes, and existing/missing launcher checks, with both legacy and native quoting controls. The final 25 added cases were also run in an isolated process with the previously published parser: 12 failed and 13 passed. Four isolated mutations were caught: disabling compatibility (8 failures), removing the launch quote guard (1), removing the readiness quote guard (1), and using POSIX parsing for the entire Windows command (15). A seeded 50,000-case check through the native producer and both consumers reported no argv or readiness-token mismatches. Whole-branch checks: ```powershell $prFiles = @(git diff --name-only origin/main...HEAD -- '*.py') uv run --no-sync ruff check @prFiles uv run --no-sync ruff format --check @prFiles $production = @($prFiles | Where-Object { $_ -notlike 'tests/*' }) uv run --no-sync ty check @production uv run --no-sync lint-imports uv run --no-sync pre-commit run --from-ref origin/main --to-ref HEAD uv run --no-sync python scripts/check_commit_messages.py origin/main..HEAD uv run --no-sync python scripts/check_source_language.py origin/main...HEAD uv run --no-sync python scripts/check_large_files.py origin/main...HEAD git diff --check origin/main...HEAD ``` All passed: 24 Python files passed Ruff checks; production type checks passed; all 10 import contracts were kept. The pinned commitlint CLI also passed over the full branch range. The final documentation-only follow-up changes no executable behavior. Limits: a broader Windows selection stopped after 283 passing tests at `test_cli_probe_reports_the_resolved_absolute_path`, whose bare POSIX shell fixture is not resolved as a Windows executable. This is not counted as a passing suite. The ACP regression checks the arguments delivered to process creation; it does not claim to verify the existing POSIX-only process-group shutdown path on Windows. The local Docker daemon was unavailable, so no new local Linux or macOS run is claimed. The full repository suite and a live WebUI dispatch were not run locally for this revision. The pre-submit sweep covered the complete branch diff, all command producers and consumers, native/legacy quoting boundaries, installer fallbacks, failure paths, test strength, layer contracts, repository gates, and these disclosed verification limits. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed No screenshot update applies. Parser docstrings describe the legacy-path compatibility and how to preserve literal quotes. ## Risk - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes Compatibility applies to single-quoted absolute path tokens, including path values after `=`; it does not enable general POSIX shell syntax on Windows. Non-path apostrophes remain literal. To preserve literal single quotes around path-looking data, quote the entire argument with native double quotes. Existing stored path tokens are read compatibly without rewriting configuration; newly generated commands keep native quoting. POSIX launch parsing is unchanged. Readiness preserves foreign absolute-path spellings and checks launcher existence; it is not a general command validator. Older installers reject paths their Raven version cannot represent before changing the roster. ACP batch shims execute configured server arguments through cmd.exe. Prompt-bearing CLI and Kimi calls do not opt into batch lookup. A CLI agent installed only as an npm shim still needs an explicit executable path. Reverting the squash commit restores the prior behavior. ## Related Issues #880 --------- Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com>
… signs (#912) ## Summary Recursive Windows environment expansion could grow without an output bound, while a literal percent in one PATH component could hide a later valid reference. Resolve names with memoization, detect cycles, limit reference depth and output size, and keep valid nested and case-insensitive references. An invalid expansion fails the capture rather than publishing a truncated PATH; refresh retains the prior environment. Tests cover literal percent signs, unknown names, nesting, small injected limits, non-string registry values and explicit program paths. Capture descriptions now distinguish POSIX and Windows, including the settings writer's and the setup wizard's notes on `~/.raven/env`, which only POSIX cli and acp sub-agents read through their login shell. ## Type - [x] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed At this head, rebased onto main, Linux, Python 3.12: - `uv run --frozen --python 3.12 --all-extras pytest -q` with the proxy variables unset: 3 failed, 29654 passed, 158 skipped. The same 3 fail on main at the merge base on this machine (an npm on PATH, a root-only permission check, a LibreOffice conversion); 0 introduced. - `uv run --frozen --python 3.12 --all-extras pytest tests/test_subagent_host_env.py -q`: 29 passed. - `ruff check`, `ruff format --check`, `lint-imports`, `ty check` and `deptry` over the Makefile targets, `commitlint`, the three repository gate scripts and `pre-commit run --from-ref origin/main --to-ref HEAD`: passed. Native Windows, Python 3.12. PYTHONUTF8=1; the existing environment was selected through UV_PROJECT_ENVIRONMENT, and PYTHONPATH pointed at this checkout. ```text uv run --no-sync pytest -o addopts= tests/test_subagent_host_env.py -k "literal_percent or refuses_cycles or bounds_output or bounds_reference or invalid_expansion or non_strings or explicit_program" -q --tb=short --show-capture=no ``` Result: 13 passed in the targeted selection. The full host-environment file previously produced 26 passed and 3 failed on Windows. The same three failures reproduced on the unmodified base: two os.killpg calls and an executable-fixture lookup. On Linux the whole file passes at this head; the three Windows failures are the base's. Whole-branch gates: ```powershell $changed = @(git diff --name-only origin/main...HEAD -- '*.py') $production = @($changed | Where-Object { $_ -like 'raven/*' }) uv run --no-sync ruff check @Changed uv run --no-sync ruff format --check @Changed uv run --no-sync ty check @production uv run --no-sync lint-imports uv run --no-sync pre-commit run --from-ref origin/main --to-ref HEAD uv run --no-sync python scripts/check_commit_messages.py origin/main..HEAD uv run --no-sync python scripts/check_source_language.py origin/main...HEAD uv run --no-sync python scripts/check_large_files.py origin/main...HEAD git diff --check origin/main...HEAD ``` All listed gates passed. The whole-branch review covered callers, ownership, guard boundaries, failure paths, regression strength, architecture and compatibility. No screenshot change applies; directly affected code documentation is updated where needed. The full repository suite and Linux/macOS execution were not run locally; the local Docker Linux engine was unavailable. ## Risk - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes Each expansion is limited to 32,767 characters and 16 references deep. Cyclic, deeper or oversized input now takes the existing capture-failure fallback instead of returning unresolved or excessively expanded text. PATH ordering and duplication policy are unchanged. Revert the squash commit to roll back. ## Related Issues #880
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