Skip to content

fix(subagent): start agents on windows from a stored launch command - #864

Open
LivXue wants to merge 20 commits into
mainfrom
fix/windows_subagent_launch
Open

LivXue wants to merge 20 commits into
mainfrom
fix/windows_subagent_launch

Conversation

@LivXue

@LivXue LivXue commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Summary

Stored Windows launch commands were parsed with POSIX rules, which removed backslashes from interpreter and launcher paths. Launch consumers now share platform-aware parsing and quoting, and bare program names are resolved once on the child's PATH.

  • Preserve quoted Windows and POSIX paths in readiness checks. Literal or escaped quotes in preceding arguments no longer hide a missing launcher from discovery or stale-row detection.
  • Share path substitution across discovery, registration, scaffold smoke checks, the generated installer, and all five shipped installers. Under older Raven versions without the shared resolver, installers retain plain-path compatibility but refuse unsupported whitespace in command paths before writing a row. A working directory passed separately remains supported.
  • Quote Raven's own ACP executable and forwarded config path for the launch platform. Windows quoting preserves embedded quotes and trailing backslashes.
  • Resolve programs on the child's PATH, in directory order with executables preferred within each directory. ACP launches can resolve batch shims for configured server commands; CLI and Kimi prompt arguments do not opt into that lookup.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

Checked on Windows after rebasing onto the current integration branch. Commands use the existing uv environment with this checkout and its plugin sources on PYTHONPATH, and PYTHONUTF8=1.

  • uv run --no-sync pytest -n 0 tests/test_utils_commands.py tests/test_subagent_vendored_agents.py tests/test_subagent_builtin_agents.py tests/test_subagent_registry.py tests/test_cli_agents_commands.py::test_the_generated_installer_on_older_raven_checks_paths_before_registering -q -x: 237 passed.
  • Expanded comparison: uv run --no-sync pytest -n 0 tests/test_utils_commands.py tests/test_subagent_vendored_agents.py tests/test_subagent_builtin_agents.py tests/test_subagent_registry.py tests/test_cli_agents_commands.py -q --tb=no --junitxml $prReport, with $prReport set to a temporary XML file. Fixed tree: 298 passed, 1 skipped, 7 failed, 4 errors. The rebased PR before the two follow-up fixes: 249 passed, 1 skipped, 12 failed, 4 errors. Both runs use the platform-safe permission-test skip needed to collect the CLI module on Windows. Comparing every failing test ID, including setup errors, found no introduced failures.
  • The remaining Windows failures cover existing command-string/path-separator assertions, POSIX permission behavior, unavailable os.fchmod, and insufficient privileges for symlink fixtures. They are not reported as passing tests. The complete repository suite and a live WebUI dispatch were not run for this revision.
  • Replacing the readiness and installer fixes with the old implementations made 28 regression cases fail. An inline uv run --no-sync python - probe compared readiness and launch parsing for 3,110 generated quote cases across both platform branches; all agreed.
  • $prPythonFiles = @(git diff --name-only origin/main...HEAD -- '*.py'), followed by uv run --no-sync ruff check @prPythonFiles and uv run --no-sync ruff format --check @prPythonFiles: all 24 changed Python files passed.
  • uv run --no-sync lint-imports: 10 contracts kept, 0 broken.
  • uv run --no-sync python scripts/check_source_language.py origin/main and uv run --no-sync python scripts/check_large_files.py origin/main...HEAD: passed.
  • uv run --no-sync python scripts/check_commit_messages.py origin/main..HEAD and node node_modules/@commitlint/cli/cli.js --from origin/main --to HEAD --config commitlint.config.cjs: passed.
  • git diff --check origin/main...HEAD: passed.

The pre-submit sweep covered the full branch diff, command producers and consumers, platform behavior, older-version fallbacks, error paths, architecture, repository conventions, and test strength. No additional blocker was found.

  • Relevant tests pass locally
  • Relevant lint / type checks pass locally
  • User-facing docs or screenshots are updated when needed

No UI or documented command syntax changed; no documentation or screenshot update is needed.

Risk

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

Windows commands retain native separators and quoting. POSIX launches still use shlex. Readiness preserves foreign absolute-path spellings and checks launcher existence; it is not a general command validator. Host ACP commands are derived when the roster loads, so no stored-command migration is needed.

Older installers now reject command paths they cannot represent, with an upgrade or space-free-path remedy, before changing the roster. Plain command paths and a separately passed working directory remain supported.

On Windows, ACP batch shims execute configured server arguments through cmd.exe. Prompt-bearing CLI and Kimi launches retain their existing lookup restriction. 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

@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: Windows CLI executable resolution and the relevant test regressions must be fixed before merge.

I reviewed the full diff and the affected launch, probe, manifest-resolution, and compatibility call paths. I also checked the branch history and PR description; the repository rules in AGENTS.md/CLAUDE.md and CONTEXT-MAP.md; backward compatibility for stored CLI rows and cross-shaped commands; test changes for weakening/skips; and the inner-layer import direction. The three concrete issues are inline.

Verification:

  • PYTHONPATH=.:plugins-dist/everos-memory:plugins-dist/design-engine:plugins-dist/ppt-engine uv run pytest -n 0 tests/test_utils_commands.py tests/test_subagent_vendored_agents.py::TestAWindowsShapedCommand::test_a_missing_windows_launcher_disables_the_row_with_the_token_named tests/test_subagent_vendored_agents.py::TestASpaceInTheResolvedPath::test_resolution_quotes_a_spaced_subagent_dir -q -> 11 passed, 2 failed.
  • PYTHONPATH=.:plugins-dist/everos-memory:plugins-dist/design-engine:plugins-dist/ppt-engine uv run pytest -n 0 tests/test_subagent_probe.py -q -> 66 passed.

Comment thread raven/agent/subagent/backends/cli_agent.py
Comment thread raven/agent/subagent/vendored_agents.py Outdated
Comment thread tests/test_subagent_vendored_agents.py Outdated

@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: the three existing Windows launch findings remain unresolved.

This revision only reformats one assignment in vendored_agents.py; it does not change the CLI executable resolution, cross-shaped command parsing, or the Windows-parser test setup raised in the existing threads. I reviewed the delta and rechecked it against the full diff, affected callers, repository rules and architecture, backward compatibility, and test changes. No new finding was introduced by the formatting commit.

Focused verification on this head:
PYTHONPATH=.:plugins-dist/everos-memory:plugins-dist/design-engine:plugins-dist/ppt-engine uv run pytest -n 0 tests/test_utils_commands.py tests/test_subagent_vendored_agents.py::TestAWindowsShapedCommand::test_a_missing_windows_launcher_disables_the_row_with_the_token_named tests/test_subagent_vendored_agents.py::TestASpaceInTheResolvedPath::test_resolution_quotes_a_spaced_subagent_dir -q -> 11 passed, 2 failed, with the same two failures already reported.

@LivXue
LivXue force-pushed the fix/windows_subagent_launch branch from 59c0955 to 1820ec4 Compare October 6, 2026 07:59

@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: the three open Windows launch findings remain unresolved.

The tree is identical to the previous revision; only the second commit message changed from style(subagent) to refactor(subagent). I rechecked the full diff, affected callers, repository rules and architecture, backward compatibility, and test changes. The commit-message correction introduces no new code finding, but it does not address the open findings.

Focused verification:
PYTHONPATH=.:plugins-dist/everos-memory:plugins-dist/design-engine:plugins-dist/ppt-engine uv run pytest -n 0 tests/test_utils_commands.py tests/test_subagent_vendored_agents.py::TestAWindowsShapedCommand::test_a_missing_windows_launcher_disables_the_row_with_the_token_named tests/test_subagent_vendored_agents.py::TestASpaceInTheResolvedPath::test_resolution_quotes_a_spaced_subagent_dir -q -> 11 passed, 2 failed, unchanged.

@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; suggestions only, and they are marked inline.

The two reproduced regressions are fixed, and I withdrew the remaining prior blocker because it was pre-existing and has an explicit-path workaround. I reviewed the new delta and full PR diff, affected launch/probe callers and history, AGENTS.md/CLAUDE.md/CONTEXT-MAP.md rules, backward compatibility, test-strength changes, and the inner-layer import direction. One narrow POSIX spaced-path readiness edge is marked inline as a follow-up; it does not meet the fourth-round blocking bar because it needs a spaced product root plus a missing launcher and can be escaped by relocating the product.

Verification:
PYTHONPATH=.:plugins-dist/everos-memory:plugins-dist/design-engine:plugins-dist/ppt-engine uv run pytest -n 0 tests/test_utils_commands.py tests/test_subagent_vendored_agents.py tests/test_subagent_probe.py -q -> 180 passed.

Comment thread raven/utils/commands.py Outdated
Comment thread raven/agent/subagent/vendored_agents.py
@0xKT

0xKT commented Oct 6, 2026

Copy link
Copy Markdown
Member

Not a blocker. Four more findings from the same pass, none of which holds the merge

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.
button. The one that does is in the review thread on vendored_agents.py.

Gates on the merged tree: gates.sh ruff/lint-imports/commit/large/lang rc=0.

1 -- command_quote does not double a trailing backslash run, so its own round-trip is false

raven/utils/commands.py:122-133. The Windows arm wraps in double quotes and escapes only the
inner quote. The Win32 rule is that a run of backslashes immediately before the CLOSING quote
must be doubled, or that quote is consumed as an escaped one. Forcing only
raven.utils.commands.os.name to "nt" -- the same thing your own _as("nt") helper does at
tests/test_utils_commands.py:19-20:

C:\Program Files\node\npx.cmd  -> "C:\Program Files\node\npx.cmd"  -> round-trips   <- control
C:\Users\me\agents\            -> "C:\Users\me\agents\"            -> ValueError: unbalanced quotes
C:\Users\me\agents\\           -> "C:\Users\me\agents\\"           -> C:\Users\me\agents\   (one lost, SILENTLY)

The docstring at :131-132 states the property this breaks: "Producing this platform's
quoting is what makes a template round-trip back into the same argv it was built from."

Reachability, which is why this is not blocking: the only route is SUBAGENT_PYTHON
(vendored_agents.py:418, raw operator bytes with only .strip()). The other call site,
command_quote(str(folder)), cannot produce a trailing separator -- folder is
manifest.parent from a */subagent.json glob, always one level below root. A
SUBAGENT_PYTHON ending in \ names a directory and was never going to launch; the cost is
that the failure becomes "unbalanced quotes" instead of "no such file". The two-backslash arm
is the one worth fixing: it is silent.

2 -- the round-trip property is asserted by name, not by test

tests/test_utils_commands.py:61-65 is docstringed for the round trip and samples one value
with a space, no inner quote and no trailing backslash. Three mutations of command_quote
survive the whole file at 11 passed; the control (quote returns the value unquoted) fails 2,
including that very test, so the suite does reach it. The sharpest survivor escapes an inner
quote as "" -- the other escaping real CommandLineToArgvW understands -- while
_split_windows at :53-56 only toggles quote state on a bare ". Under that mutant the
module's own quoter and its own splitter disagree (C:\dir\a"b round-trips to C:\dir\ab)
and the test named for the round trip still passes.

3 -- the guard in _resolve_executable_windows is unasserted

raven/utils/commands.py:147-149, test at tests/test_utils_commands.py:82-87. The test
monkeypatches shutil.which to lambda exe: None, and the guarded line is
return shutil.which(exe) or exe -- so with which() pinned to None both branches return
exe and both assertions hold whether or not the guard ran. Deleting the guard entirely leaves
11 passed; the control (the function always returning exe) fails
test_windows_resolves_a_bare_extensionless_name, so the suite does reach the function. A
which() returning a wrong-but-truthy path instead of None kills it. Production behaviour on a
real Windows host is correct -- this is a test hole, not a defect.

4 -- _is_absolute_path's docstring states the constraint this PR exists to lift

raven/agent/subagent/vendored_agents.py:458-461: "Tokens come from str.split(), so a path
containing spaces arrives here as fragments. The command templates the manifests and each
install.py emit keep their paths space-free, and that constraint is cheaper than
re-tokenizing every stored row's command line." Both halves are false at head: the two callers
pass command_tokens output (:477 and :920), and the PR's own new test class is named
TestASpaceInTheResolvedPath. That paragraph is the stated safety argument for judging shape
on a whitespace split, so a later reader who trusts it reasons from the wrong tokeniser.

Stated, not filed

  • The Windows arm the PR set out to fix does work. Probed both directions:
    "C:\Program Files\gone\python.exe" C:\gone\run.py keeps the quoted path as ONE token and
    _launcher_missing returns it (fail-closed), where the base str.split() gave
    ['"C:\Program', 'Files\gone\python.exe"', ...]. The regression in the thread is confined to
    the POSIX convention this PR introduced.
  • The shlex.split sweep is complete for what it claims: all eight launch readers at the base
    are converted, and the survivors under raven/ (permissions/rules.py x4,
    rpc/methods/{command_dispatch,input,slash_routing}.py, cli/ops_connection_commands.py)
    read slash commands and permission patterns, not launch commands.
  • _split_windows diverges from real CommandLineToArgvW in two documented places -- ""
    inside quotes yields nothing rather than a literal ", and an unbalanced quote raises where
    Windows takes the rest as one token. Neither is filed: command_argv's own text says it
    raises "the same class callers catch today", and in this codebase the real parser never sees
    the string. Worth a sentence in the docstring naming the narrowing.
  • The new module's docstring uses "product", which CONTEXT.md's Discovered agent
    _Avoid_ list retires for prose. Checked and dropped: the surrounding modules use it the same
    way throughout, so this PR is consistent with its neighbours rather than introducing it.

@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; suggestions only, and they are marked inline.

The POSIX quoted-root defect is fixed on this revision. command_tokens now groups both quote forms, and the added end-to-end tests drive discover_product_rows over a spaced root with both a missing and present launcher, covering the fail-closed and ready outcomes. I reviewed the delta against the full PR, affected callers and history, repository rules and architecture, backward compatibility, and test strength. I found no new blocker. The previously noted bare-name Windows CLI .cmd behavior remains a pre-existing follow-up with an explicit-path workaround.

Verification:
PYTHONPATH=.:plugins-dist/everos-memory:plugins-dist/design-engine:plugins-dist/ppt-engine uv run pytest -n 0 tests/test_utils_commands.py tests/test_subagent_vendored_agents.py tests/test_subagent_probe.py -q -> 185 passed.

@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; suggestions only, and they are marked inline.

The shared resolver makes discovery, registration, and newly generated installers agree, and I reviewed the delta against the full PR, affected callers/history, repository rules and architecture, backward compatibility, and test strength. One incomplete consumer remains inline: the default scaffold smoke still whitespace-splits the now-quoted command. I am carrying it as a nonblocking follow-up under the late-round bar because --no-smoke provides a working path through creation/registration, although the default workflow and four existing CLI tests currently fail.

Verification:
PYTHONPATH=.:plugins-dist/everos-memory:plugins-dist/design-engine:plugins-dist/ppt-engine uv run pytest -n 0 tests/test_utils_commands.py tests/test_subagent_vendored_agents.py tests/test_subagent_probe.py tests/test_cli_agents_commands.py tests/test_cli_subagent_setup.py -q -> 298 passed, 4 failed, 1 skipped. The four failures are the whitespace-policy CLI cases named inline.

Comment thread raven/cli/agents_commands.py

@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; suggestions only, and they are marked inline.

This revision updates the CLI assertions to the new quoted-path success contract, but the previously reported nonblocking smoke follow-up remains unresolved: the default smoke still raw-splits quoted commands. The spaced-home and --here cases therefore still write the scaffold and exit nonzero. The SUBAGENT_PYTHON case now also expects success from a deliberately nonexistent interpreter, which readiness correctly rejects. I reviewed the test-only delta against the production flow, affected callers, repository rules and architecture, backward compatibility, and test strength; no new finding beyond the already recorded follow-up emerged.

Verification:
PYTHONPATH=.:plugins-dist/everos-memory:plugins-dist/design-engine:plugins-dist/ppt-engine uv run pytest -n 0 tests/test_utils_commands.py tests/test_subagent_vendored_agents.py tests/test_subagent_probe.py tests/test_cli_agents_commands.py tests/test_cli_subagent_setup.py -q -> 299 passed, 3 failed, 1 skipped.

@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: the new spaced-path tests must assert the discovery-only contract or request registration.

The production smoke fix is correct: it now parses the stored command with command_argv, and the spaced-root invocations reach exit code 0. I reviewed the delta against the full PR, affected CLI/discovery callers and history, repository rules and architecture, backward compatibility, and test strength. The one new finding is inline.

This meets the late-round blocking bar: this revision introduces the failing assertions; every relevant Linux test run reaches them; and the branch has no passing-test path until the tests either add --register or inspect the discovered row.

Verification:
PYTHONPATH=.:plugins-dist/everos-memory:plugins-dist/design-engine:plugins-dist/ppt-engine uv run pytest -n 0 tests/test_utils_commands.py tests/test_subagent_vendored_agents.py tests/test_subagent_probe.py tests/test_cli_agents_commands.py tests/test_cli_subagent_setup.py -q -> 299 passed, 3 failed, 1 skipped.

Comment thread tests/test_cli_agents_commands.py

@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; suggestions only, and they are marked inline.

The registration-contract blocker is fixed: the three spaced-path tests now opt into registration before reading the pinned roster, and the production smoke path continues to parse stored commands through command_argv. I reviewed the delta and the full PR diff, relevant callers and history, repository rules and architecture constraints, backward compatibility, and whether the tests were weakened. The pre-existing bare-name Windows CLI .cmd resolution limitation remains a follow-up with an explicit executable path as a working escape hatch.

Verification: PYTHONPATH=.:plugins-dist/everos-memory:plugins-dist/design-engine:plugins-dist/ppt-engine uv run pytest -n 0 tests/test_utils_commands.py tests/test_subagent_vendored_agents.py tests/test_subagent_probe.py tests/test_cli_agents_commands.py tests/test_cli_subagent_setup.py -q (302 passed, 1 skipped); git diff --check github/main...HEAD (clean).

LivXue added a commit that referenced this pull request Oct 9, 2026
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>
Comment thread raven/utils/commands.py
0xKT pushed a commit that referenced this pull request Oct 9, 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
LivXue force-pushed the fix/windows_subagent_launch branch from 7e4256f to 51191e7 Compare October 9, 2026 14:44

@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; suggestions only, and they are marked inline.

The new revision fixes the Windows single-quote failure: host_raven_acp_command() and its forwarded --config path now use the same platform quoter as the launch parser. The added tests cover sibling and PATH launchers, spaced and unspaced paths, both simulated platforms, and config forwarding through command_argv. Range-diff confirms the prior eight reviewed commits are unchanged rebases and this is the only substantive delta.

I covered the repository rules, full PR diff and new delta, callers and history, backward compatibility, architecture constraints, and whether tests were weakened. The pre-existing bare-name Windows CLI .cmd resolution limitation remains only a follow-up with an explicit executable path as a working escape hatch.

Verification: PYTHONPATH=.:plugins-dist/everos-memory:plugins-dist/design-engine:plugins-dist/ppt-engine uv run pytest -n 0 tests/test_utils_commands.py tests/test_subagent_builtin_agents.py tests/test_subagent_vendored_agents.py tests/test_subagent_probe.py tests/test_cli_agents_commands.py tests/test_cli_subagent_setup.py -q (342 passed, 1 skipped); git diff --check github/main...HEAD (clean).

@LivXue
LivXue force-pushed the fix/windows_subagent_launch branch from 51191e7 to 7ce8b10 Compare October 10, 2026 03:49
@LivXue

LivXue commented Oct 10, 2026

Copy link
Copy Markdown
Member Author

Answering the board note above, item by item, at 7ce8b10a6. The same push rebases onto main (c3d282130).

  1. command_quote and a trailing backslash: fixed in fix(utils): double the backslashes a windows quote would swallow (e20b07836). Every run of backslashes in front of a quote, the closing quote included, is now doubled, so C:\Users\me\agents\ and C:\Users\me\agents\\ both split back as themselves.
  2. The round trip asserted by name: test_a_quoted_windows_token_round_trips now runs 11 tokens (one and two trailing backslashes, a backslash before an inner quote, quotes at both ends, a UNC path, the empty string) at either end of the line, with a POSIX twin over 7. Your three survivors (an inner quote escaped as "", left bare, dropped) now fail 6 tests each, and backslash runs left single fail 4.
  3. The unasserted guard: _resolve_executable_windows went with launch_argv (below), and the same guard in resolve_program is asserted with a which() that answers every name. Deleting the path guard, or letting an unstartable suffix through, fails test_on_windows_only_a_bare_startable_name_is_looked_up.
  4. _is_absolute_path's docstring now says its tokens come from command_tokens (347b5e2b0).

From the stated-not-filed list, _split_windows' docstring now names its two narrowings from the real parser, neither of them a spelling command_quote produces.

#880's lookup is folded in: the ACP launch looks a bare program up once, on the child's PATH, directory by directory with .exe first (f4103d5c4). resolve_program takes batch_files. The ACP launch passes it, because its argv is configuration and an npm-installed server has only a .cmd; the cli launch and the kimi ask do not, because their argv carries the prompt. Each of the three call sites has its own test.

Also in this push:

  • The shipped install.py files write through the quoting rule (604747147).
  • Under a raven without raven.utils.commands (v0.2.0 through v0.2.4), the shipped and scaffold installers write the unquoted row instead of failing on the import (7e6dbe1c6 for the scaffold).
  • Text that still described the removed whitespace refusal is updated (81677c5de).
  • The author of fix(agent): quote host acp paths for the launch platform is corrected. It was committed on a machine whose git address resolves to an unrelated GitHub account. Its patch-id is unchanged.

Verification and the mutation check are in the description.

@LivXue
LivXue requested review from 0xKT and gloryfromca October 10, 2026 03:51

@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; suggestions only, and they are marked inline.

The nine follow-up commits are sound. Windows quoting now round-trips backslashes and embedded quotes; ACP program lookup uses the child's PATH and permits batch shims without extending that exposure to prompt-bearing CLI launches; shipped and generated installers use the shared resolver while retaining their older-Raven fallback. The newly posted resolutions of the POSIX probe and host-ACP quoting failures match the current implementation and tests.

I covered the repository rules, full diff and revision delta, callers and history, backward compatibility, architecture constraints, and whether tests were weakened. All threads I opened are resolved. The pre-existing bare-name Windows CLI .cmd limitation remains only a follow-up with an explicit executable path as a working escape hatch.

Verification: PYTHONPATH=.:plugins-dist/everos-memory:plugins-dist/design-engine:plugins-dist/ppt-engine uv run pytest -n 0 tests/test_utils_commands.py tests/test_subagent_builtin_agents.py tests/test_subagent_vendored_agents.py tests/test_subagent_probe.py tests/test_subagent_host_env.py tests/test_subagent_kimi_code.py tests/test_subagent_third_party.py tests/test_subagent_acp.py tests/test_cli_agents_commands.py tests/test_cli_subagent_setup.py -q (998 passed); git diff --check github/main...HEAD (clean).

@0xKT

0xKT commented Oct 10, 2026

Copy link
Copy Markdown
Member

Not a blocker -- from the re-acceptance pass on head 7ce8b10 (merged onto main 289426c), over the fix commits that came after the first pass.

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. A quote in a hand-written manifest still splits the judge from the spawn. _split_shape (raven/utils/commands.py:100) groups on both quote characters with no escape, while the spawn splits with shlex on POSIX and _split_windows on nt. A template with an escaped quote before the launcher -- {PYTHON} --name=O\'Brien {SUBAGENT_DIR}/run.py --acp, or the same with \" -- opens a quote the judge never closes. Its tokens are ['<py>', '--name=O\Brien <root>/run.py --acp'], so _launcher_missing has no absolute token to check and a folder whose run.py is gone reads ready, while the spawn reads ['<py>', "--name=O'Brien", '<root>/run.py', '--acp'] and fails at dispatch. A double-quoted "--name=O'Brien" agrees on both sides. On the nt branch a bare --name=O'Brien does the same (noted on the earlier thread). Only a user-edited manifest can carry this -- none of the shipped ones does -- and main's str.split judge disabled the same rows. The _split_shape docstring (commands.py:80-93) still says single quotes are literal and that an unclosed quote answers "the path is not there".
  2. Under an older raven, the generated installer now registers a row that cannot launch. raven/templates/agents_scaffold/install.py:26-32 falls back to plain substitution when raven.utils.commands is missing, so a folder with a space registers a row that splits at the space, where the previous installer refused the folder and said why. The comment there says this is deliberate; a refusal under the older raven would at least tell the user what to do.

Checked on the merged tree: the machine gates pass. The acceptance pass also ran the full suite: nothing red because of this PR; the reds are the known environment-only ones plus one process-group timing test that also fails on main under load.

LivXue and others added 6 commits October 11, 2026 01:18
A subagent row keeps its launch as one string, and where that string is
split decided whether the spawn ever saw what was written. Every launch
and probe reached for POSIX-mode shlex.split on any host, so on Windows
the backslashes every interpreter and launcher path carries were eaten
by the escape rules before CreateProcess ran: C:\Users\..\python.exe
reached the spawn as C:Users..python.exe, and a bare npx was handed to
a launcher that only finds npx.cmd by full path.

Add raven/utils/commands as the one interpretation. command_argv reads a
stored command by CommandLineToArgvW's own backslash and quote rules on
Windows (shlex elsewhere), command_quote produces a token that survives
that split, and launch_argv adds the PATHEXT resolution of a bare
extensionless argv[0]. AcpClient.launch, the cli backend, the codex
dialect and the probes all parse through it, and the vendored manifests
quote {PYTHON} and {SUBAGENT_DIR} when they resolve. A product whose
interpreter sits under C:\Program Files now starts instead of failing
with a mangled path.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
The launcher probes judge the command a row holds, not spawn it, but they
tokenised through the spawn split. On POSIX that split is shlex, which eats
the backslashes of a Windows drive path: C:\gone\python.exe reached
_is_absolute_path as C:gonepython.exe and read as no absolute path at all,
so a Windows-shaped product whose launcher was gone read as ready on a
POSIX scan -- the fail-closed check the class docstring promises.

Add command_tokens for the judging arm: split on whitespace and double
quotes but keep every other byte, so a Windows token keeps its shape and a
quoted one stays whole, on any host. The spawn arm still parses by the
host's own rules in command_argv.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
command_tokens grouped double quotes only, but command_quote emits POSIX
paths under shlex.quote's single quotes. A product whose tree or
interpreter path needs quoting (a spaced root, a paren) then produced a
launcher token that began with an apostrophe, so _is_absolute_path
called it relative, _launcher_missing checked nothing, and a manifest
whose run.py was gone reached the roster enabled=True -- the fail-open
the readiness gate exists to refuse. A reviewer reproduced it end to end
through discover_product_rows with a control and a base control.

Group single quotes as well as double in _split_shape, and drive the
agreement through discover_product_rows over a spaced root: a missing
launcher stays disabled, a present one stays ready. Windows already read
a single quote as a literal byte, so this changes only the POSIX half.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
Discovery quoted the manifest placeholders, but raven agents new refused a
whitespace path outright and install.py exited with one, while
_register_row substituted with no quoting at all -- so the same folder
registered three different ways depending on which door wrote the row.
The comment agents new carried said quoting was a seam it must not open,
which the discovery half had already opened.

Resolve all three through resolve_subagent_command: the interpreter and
agent root are quoted with command_quote when the field is split back
into argv (command / resumeCommand) and left plain for cwd, and every
producer calls it. Shipped manifests already use a forward slash, so the
quoted root and its launcher stay one token to either parser. The two
whitespace refusals go away -- a spaced install path or interpreter now
registers and actually starts, matching what discovery lists.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
Four agents-new and installer tests pinned the whitespace refusal this PR
removes: with the placeholder quoting unified, a spaced home, working
directory, or SUBAGENT_PYTHON scaffolds and registers with the path quoted
into the command rather than being refused. The assertions follow the new
behaviour -- the command tokenises to the launcher path and interpreter as
single argv entries. The collection of this suite is Linux-only in CI
(os.geteuid), so these run there.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
LivXue and others added 14 commits October 11, 2026 01:18
The smoke check split its command on bare whitespace, so a spaced
interpreter or root -- now quoted into the roster command -- reached
Popen as fragments and the handshake failed with "can't open file
'...space'". Run the smoke through command_argv so it spawns exactly the
argv a dispatch would. The spaced-path cli assertions use real symlinked
interpreters and read the command through command_argv rather than a
prefix match, so they assert the parsed argv on any host.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
The three spaced-path assertions read the registered row, but plain
raven agents new only scaffolds the folder -- the row lands only with
--register -- so get_agents returned nothing and the unpack failed.
Pass --register (and keep --no-smoke off, so the smoke covers the spaced
launch these tests exist to prove).

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
Use the shared command quoter for the host Raven executable and its
forwarded config path so the Windows parser receives the original argv.
Cover sibling and PATH launchers on both platforms, including spaced
paths, and assert config forwarding through the production parser.

Co-authored-by: Codex <noreply@openai.com>
command_quote's Windows arm wrapped a token in double quotes and escaped
only an inner quote. CommandLineToArgvW halves a run of backslashes
wherever a quote follows it, the closing quote included, so a token
ending in one backslash came back as an unbalanced-quote error, a token
ending in two lost one without a word, and a backslash in front of an
inner quote closed the quoted run early. The docstring promised a round
trip for any token; SUBAGENT_PYTHON is the route a trailing backslash
takes in today.

Every run of backslashes in front of a quote is now doubled. The round
trip is asserted over a battery of tokens on both arms, at either end of
the line, and the splitter's docstring names the two places it is
narrower than the real parser.

Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
The acp launch resolved a bare program name twice. launch_argv searched
the gateway's own PATH with PATHEXT, so npx found npx.cmd, and
resolve_program then searched the child's PATH for an .exe only. The
first answer won whenever it found the name, so an acp server that is a
batch file and was installed after raven started, which the refreshed
capture and the probe both find, still could not start, and one that
sits in two places started from wherever the gateway's PATH pointed.

The two are now one lookup on the child's PATH. resolve_program takes
batch_files: the acp launch passes it, because an acp server's argv is
configuration and its turns travel over stdio, so a .bat or .cmd may
stand in for the program there. The cli launch and the kimi ask leave
it off, since their argv carries the prompt and a batch file hands its
arguments to cmd.exe's parser. PATH directories are searched in order
with .exe first within each, the order a terminal finds the name in.
launch_argv and its gateway-PATH lookup are gone.

The guard that keeps a path or an unstartable suffix from being looked
up is asserted with a lookup that answers every name; the old test's
lookup answered none, so the guard could be deleted with the file green.

Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
_is_absolute_path's docstring still said its tokens came from
str.split(), so a path with spaces arrived in fragments, and that the
manifests keep their paths space-free to avoid it. Neither has held
since both launcher probes moved to command_tokens and the row
producers started quoting, and the paragraph was the stated reason the
shape judgment is safe, so a reader trusting it reasoned from the
wrong tokeniser.

Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
resolve_subagent_command says discovery, raven agents new --register
and each folder's install.py all write through it, and the five shipped
installers still substituted their paths unquoted. A folder whose path
has a space then pinned a row whose command splits that path into two
arguments, so the pinned row and the discovered row for one folder
disagreed.

The shipped installers now write through the same rule. A folder can
outlive the raven that shipped it, and no release so far (v0.2.0
through v0.2.4) has raven.utils.commands, so under an older raven the
installer writes the unquoted row it always wrote instead of failing on
the import.

Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
The scaffold's install.py imports raven.utils.commands, which no release
so far has (v0.2.0 through v0.2.4). A folder raven agents new generates
is never refreshed when raven changes version, so after a downgrade its
installer failed on that import before writing anything. It now falls
back to the unquoted row, the shape an older raven reads. Its other
raven call, add_third_party_subagent, is in every release.

Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
raven agents new no longer refuses a spaced landing or interpreter
path, and four texts still described the refusal: _resolved_python's
docstring called itself the guard's resolver, the c12 section header
listed the guard, a test's name and docstring said a clean
SUBAGENT_PYTHON passes the gate, and the smoke test said its stand-in
had to be a file because the smoke split a command with str.split. The
smoke splits with command_argv now, and each text says what its code
still does.

Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
TestResolveSubagentCommand says each host arm is exercised whichever
host the suite runs on, and its Windows round trip skipped unless the
splitter it read was already the Windows one, which on CI it never is.
Both round trips now swap the commands module's own platform read, the
way the splitter's tests do, so the Windows one runs on Linux and fails
when the Windows quoting breaks.

Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
With batch_files a name that already carries .cmd or .bat is looked up
on the child's PATH like a bare one, and nothing asserted it: limiting
the named-suffix check back to .exe and .com left all 661 tests in the
launch suites green. The lookup-guard test now resolves npx.cmd beside
npx, and that mutant fails it.

Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
test_launcher_is_gone_reads_a_quoted_spaced_path_as_one_token named an
absolute launcher that does not exist, so a whitespace split called the
row gone as well, and the test passed whichever tokeniser
_launcher_is_gone used. Its launcher now exists, which leaves the
quoted, spaced interpreter as the only missing file; putting the
whitespace split back fails the test.

Co-authored-by: Claude (claude-opus-5-5[1m]) <noreply@anthropic.com>
Read host escape rules without losing foreign absolute path shapes.
Keep literal and escaped quotes in arguments from hiding the launcher
that follows them, including an unmatched leading apostrophe on Windows.
Cover discovery, stale rows, quote round trips, drive paths and UNC paths.

Co-authored-by: Codex <noreply@openai.com>
Reject whitespace in substituted command paths before registration when
an older Raven lacks the shared quoting resolver. Apply the same rule to
the scaffold and all five shipped installers, preserving plain paths and
working directories that are not part of the command.

Exercise both refusals and supported fallback cases. Keep the Windows
test collection guard and compare launcher paths by their native meaning.

Co-authored-by: Codex <noreply@openai.com>
@LivXue
LivXue force-pushed the fix/windows_subagent_launch branch from 7ce8b10 to 144669d Compare October 10, 2026 17:29

@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; suggestions only, and they are marked inline.

The two new fixes close the re-acceptance notes without weakening coverage. command_tokens now honors host escape rules so literal quotes cannot hide a following launcher, while preserving cross-shaped absolute paths for fail-closed readiness checks. Shipped and generated installers running under an older Raven now reject spaced command paths before registration, but still accept a spaced cwd when the command does not interpolate it.

I covered the repository rules, full diff and revision delta, callers and history, backward compatibility, architecture constraints, and test strength. All threads I opened remain resolved. The pre-existing bare-name Windows CLI .cmd limitation remains only a follow-up with an explicit executable path as a working escape hatch.

Verification: PYTHONPATH=.:plugins-dist/everos-memory:plugins-dist/design-engine:plugins-dist/ppt-engine uv run pytest -n 0 tests/test_utils_commands.py tests/test_subagent_builtin_agents.py tests/test_subagent_vendored_agents.py tests/test_subagent_probe.py tests/test_subagent_host_env.py tests/test_subagent_kimi_code.py tests/test_subagent_third_party.py tests/test_subagent_acp.py tests/test_cli_agents_commands.py tests/test_cli_subagent_setup.py -q (1042 passed); git diff --check github/main...HEAD (clean).

@0xKT

0xKT commented Oct 11, 2026

Copy link
Copy Markdown
Member

Not a blocker -- three items from the re-acceptance pass on head 144669d (merged onto main 6126965). Nothing blocking was found, and the touched tests pass (262 passed across tests/test_utils_commands.py, tests/test_subagent_vendored_agents.py and tests/test_cli_agents_commands.py).

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. The Windows launch split drops or merges an argument made only of backslashes or an escaped quote. In _split_windows, the backslash branch (raven/utils/commands.py:41-57) never sets token_started, so such a token is dropped at the end of the line or glued onto the next one. The judge's copy of the same rule in _split_shape does set it (:99-100), so readiness judges a different argv from the one that starts. Measured with os.name patched to nt, each row built by subprocess.list2cmdline(want):
    • "C:\Program Files\agent\agent.exe" --root \ acp launches as [..., '--root', '\acp'];
    • prog \ launches as ['prog'];
    • prog \\ b launches as ['prog', '\\b'].
      Rows that command_quote writes are always quoted, so only hand-written Windows rows reach this. Adding token_started = True as the first line of the backslash branch fixes it: a run of backslashes always puts at least one character into the token. With that line, a command_argv(list2cmdline(argv)) == argv round trip over 50000 random argvs fails 0 times (6413 on head), and the three test files above still give 262 passed. They pass unchanged on head too, so a round-trip row or two would pin it.
  2. On POSIX, the readiness split ignores newline and CR separators that the launcher splits on. _split_shape separates only on space and tab (:135), while the launcher's shlex.split also separates on \n and \r. So python\n/opt/gone/run.py --acp launches as ['python', '/opt/gone/run.py', '--acp'], but the judge reads ['python\n/opt/gone/run.py', '--acp']. main's str.split judge agreed with the launcher. The input needs an escaped newline inside a command string, so it is rare.
  3. The new module docstring says "a Windows-shaped product" (raven/utils/commands.py:14). CONTEXT.md:363 lists "product" under Avoid for a discovered agent and keeps it only in code-level product_* spellings.

This branch has not been deployed

No deployments
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