Skip to content

fix(agent): read the interactive PATH on Windows instead of a frozen login shell - #880

Merged
0xKT merged 5 commits into
mainfrom
fix/windows_login_path_probe
Oct 9, 2026
Merged

0xKT merged 5 commits into
mainfrom
fix/windows_login_path_probe

Conversation

@LivXue

@LivXue LivXue commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

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

  • 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.

  • 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_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.

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Related Issues

#864

@LivXue
LivXue requested review from 0xKT and gloryfromca October 8, 2026 17:41

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread raven/agent/subagent/backends/env.py Outdated
Comment thread raven/agent/subagent/backends/env.py Outdated
Comment thread raven/agent/subagent/probe.py Outdated
LivXue and others added 5 commits October 9, 2026 03:45
…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>
@LivXue
LivXue force-pushed the fix/windows_login_path_probe branch from 8388258 to e79c65f Compare October 9, 2026 03:49
@LivXue

LivXue commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

Round two is pushed as e79c65f. Two things the threads do not carry:

  • The branch was force-pushed to correct the first commit's author. Its noreply address resolved to an unrelated GitHub account, so it is now the author's own address, with the tree unchanged (8388258 became 3d0e7e3, same tree). Its Co-authored-by: Claude (kimi-k3) trailer is dropped as well, since that session was not a Claude model. The four commits after it are the fixes.
  • One fix answers no thread: e79c65f starts a bare program from the child's own PATH on Windows (resolve_program). With the capture fixed, the probe finds an agent installed after the gateway started, but CreateProcess searches the gateway's PATH, so the Connect that follows could not start it. Only an .exe or .com is resolved, never a .cmd shim, which would route a cli agent's prompt through cmd.exe.

Command-line splitting is left to #864; the description says where the two meet.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@0xKT

0xKT commented Oct 9, 2026

Copy link
Copy Markdown
Member

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.

  1. _expand_windows re-expands without a size bound (raven/agent/subagent/backends/env.py, the _depth > 16 cap). The cap bounds the number of passes, not the output: a variable that references itself twice doubles every pass, three times triples it. Measured here: {"A": "%A%;%A%"} already gives 524,287 characters, and with three references the same 16 passes grow it 3^16-fold, into hundreds of megabytes, inside login_shell_env, which every subagent launch awaits. Windows itself expands a stored value once. A single pass, or a cap on the result length, would remove the cliff.

  2. A literal % swallows the next reference: _expand_windows("C:\\50%;%USERPROFILE%\\bin", {"USERPROFILE": "C:\\U"}) returns the input unchanged, because %([^%]+)% reads %;% as an undefined name and resumes after it.

  3. The capture's headline overstates what it returns. _capture_windows says it hands back "the PATH a freshly opened Windows terminal would start with", but it returns stored;live (env.py:240), and on an interactive logon the live PATH already contains the stored entries, so each appears twice (measured through login_shell_env with a fake registry and a 41-entry live PATH: 869 -> 1654 characters, 78 entries, 37 of them duplicates; raven's MinGit entries land after every stored one). The union is deliberate per the comment at :235-238; only the first line needs adjusting. At that ratio a live PATH above roughly 4.3k characters doubles past the 8191-character limit cmd.exe applies to a variable it expands; what a .cmd-launched child then does was not run here.

  4. Two docs still describe the old capture on Windows: raven/config/env_file.py:3-6 and raven/cli/onboard_web.py:19-23 say every cli/acp sub-agent's environment "is a capture of $SHELL -lic". On Windows it is now raven's own environment plus the registry PATH.

  5. Three behaviours the suite does not pin. Each change below leaves tests/test_subagent_host_env.py, tests/test_subagent_third_party.py and tests/test_subagent_kimi_code.py green (345 passed), while a control mutant in the same code -- dropping the case-insensitive lookup in _expand_windows -- is caught: accepting non-string registry values (isinstance(value, str) in _read_store), removing the _depth > 16 cap, and dropping the ntpath.dirname check so a path-bearing argv[0] is looked up again. One test each would hold them.

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.

@0xKT
0xKT merged commit fc9bc98 into main Oct 9, 2026
34 checks passed
@0xKT
0xKT deleted the fix/windows_login_path_probe branch October 9, 2026 06:30
LivXue added a commit that referenced this pull request Oct 11, 2026
…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>
LivXue added a commit that referenced this pull request Oct 12, 2026
…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>
LivXue added a commit that referenced this pull request Oct 12, 2026
…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>
0xKT pushed a commit that referenced this pull request Oct 13, 2026
… 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants