Skip to content

fix: bound Windows environment expansion and preserve literal percent signs - #912

Merged
0xKT merged 2 commits into
mainfrom
fix/windows_environment_variable_expansion
Oct 13, 2026
Merged

0xKT merged 2 commits into
mainfrom
fix/windows_environment_variable_expansion

Conversation

@LivXue

@LivXue LivXue commented Oct 11, 2026 •

Copy link
Copy Markdown
Member

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

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

Verification

  • Relevant tests pass locally
  • 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.

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:

$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

  • Security impact considered
  • Backward compatibility considered
  • 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

@LivXue
LivXue requested review from 0xKT and gloryfromca October 11, 2026 16:27

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

Reviewed the full diff plus the Windows capture/launch callers and relevant history. I checked the repository rules and runtime terminology, failure/fallback behavior, compatibility of unknown and literal percent handling, cycle/depth/output bounds, and confirmed the tests add coverage without weakening existing assertions. The changes stay within the existing platform boundary and preserve PATH ordering.

Verification: PYTHONPATH="$PWD/plugins-dist/everos-memory${PYTHONPATH:+:$PYTHONPATH}" uv run pytest tests/test_subagent_host_env.py -q passed (29 tests); Ruff check and format-check passed for all changed Python files; git diff --check github/main...HEAD passed. GitHub's Windows and Python/lint checks are also green at review time.

@LivXue

LivXue commented Oct 11, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility review: no new Windows defect found in the inspected changes; one inherited test failure remains.

Reviewed all 4 changed files at head f6ab0de1521f3951f5fe6864a5f61d02e2e57ce9 against base 4acc63cebd0752652194f9e4989628da29a8a25d, including registry PATH capture, expansion limits/cycles, capture fallback, executable discovery callers and the documentation corrections.

Native Windows / Python 3.13.12, detached temporary checkout, PYTHONUTF8=1, UV_NO_SYNC=1, isolated user/Raven homes:

uv run pytest tests/test_subagent_host_env.py -k 'windows' -n 0 -x --tb=short --basetemp "$reviewRunDir/pr912/tests"
# Head: 20 passed, 1 failed, 2 deselected.

uv run pytest tests/test_subagent_host_env.py::test_windows_probe_finds_agents_on_the_captured_path -n 0 -x --tb=short --basetemp "$reviewRunDir/pr912/base-tests"
# Exact base: the same test failed with the same missing-executable result.

uv run pytest 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' -n 0 -x --tb=short --basetemp "$reviewRunDir/pr912/targeted-tests"
# Head restored: 13 passed, 16 deselected.

The inherited failure constructs an extensionless executable fixture; the probe reports missing rather than its expected attention/resolved path on this Windows host. It is not a regression introduced by this PR. The new literal-percent, cycle, size/depth, failed-refresh and explicit-path cases passed.

Limits: the tests substitute the registry; no real registry edits, installed-agent launch, or full-suite validation was performed. This is not a full compatibility certification.

@LivXue

LivXue commented Oct 11, 2026

Copy link
Copy Markdown
Member Author

No blockers in the reviewed scope.

Scope: I reviewed the full PR diff (4 changed files), including tests and documentation, and traced affected callers and contracts at f6ab0de1521f. This review covers all changed areas, not only raven/agent.

Checked literal-percent and unknown-reference handling, recursive expansion, cycle/depth/output bounds, memoization, PATH precedence, first-capture and refresh fallback, and CLI/ACP/probe consumers. Invalid expansion preserves the prior capture; the platform-specific documentation now matches the environment actually supplied to Windows children.

Verification environment: native Windows; isolated source snapshots.

Verification: uv run --no-sync pytest -o addopts= tests/test_subagent_host_env.py -q --tb=short gave 26 passed and 3 failed. All three failing IDs reproduced at the merge base: two ACP close calls use unavailable os.killpg on Windows, and one executable-lookup fixture assumes an extensionless program is runnable. The new expansion cases passed. No new finding was identified. I also read the Python 3.13 Windows review. Its conclusions are consistent; the measurements here record a separate Python 3.12 run.

git diff --check over the complete merge-base-to-head range passed. No new inline finding is being filed.

@LivXue
LivXue force-pushed the fix/windows_environment_variable_expansion branch from f6ab0de to b94466f Compare October 12, 2026 09:12

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

The tree is byte-for-byte identical to the previously reviewed revision; this rewrite only changes commit metadata. I also checked compatibility with the advanced main tip: the merge-tree is clean and the intervening changes do not touch this PR's files.

Verification: PYTHONPATH="$PWD/plugins-dist/everos-memory${PYTHONPATH:+:$PYTHONPATH}" uv run pytest tests/test_subagent_host_env.py -q passed (29 tests); git diff --check github/main...HEAD and the commit-message checker passed. The repository rules, full diff, callers/history, backward compatibility, test strength, and architecture constraints remain covered by the unchanged tree.

@LivXue

LivXue commented Oct 12, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility re-review: no new issue found in the inspected scope.

Reviewed the complete 4-file PR diff at b94466f2d550e0b2bef0bc7533a21ac021ace81e; API base 829eb28e1db877dffdd3df4f835d707e1f049d3a, merge base 4acc63cebd0752652194f9e4989628da29a8a25d. Checked registry PATH capture, case-insensitive nested expansion, literal percent signs, cycle/depth/output limits, failed-refresh fallback, explicit executable paths and launch callers.

Git checks: source tree identical to previously reviewed f6ab0de1521f3951f5fe6864a5f61d02e2e57ce9; diff whitespace check passed and the merge-tree check against the recorded API base returned exit 0.

Native Windows / Python 3.13.12, detached checkout, PYTHONUTF8=1, UV_NO_SYNC=1, isolated user/Raven/temp directories. Reused a uv environment with matching pyproject.toml/uv.lock; PYTHONPATH selected this checkout and its bundled plugin roots, and the loaded Raven source path was asserted.

PR head:

uv run pytest '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' -n 0 -x --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\fa39f19b3fcb4d7daa4ad3b9472340f8\pr912\unit-pytest'
# 13 passed, 16 deselected in 7.59s; exit 0

Limits: Registry data is substituted in these tests; no real registry edits or installed-agent launch was performed. The broader host-environment suite was not rerun; previously reported inherited executable-fixture/process-group failures are outside this selection. This scoped result does not establish full Windows compatibility.

… signs

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.
Two comments still said every sub-agent inherits the web keys through
~/.raven/env. Only POSIX cli and acp sub-agents read it, through their
login shell capture; Windows children inherit Raven's own environment.
@LivXue
LivXue force-pushed the fix/windows_environment_variable_expansion branch from b94466f to 9674348 Compare October 12, 2026 12:55

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

This revision cleanly rebases the reviewed implementation onto current main and adds two consistent documentation corrections: the onboarding overview and settings-write comment now accurately limit ~/.raven/env inheritance to POSIX CLI/ACP sub-agents. I compared the range against the prior revision and rechecked the full six-file PR diff, repository rules, callers/history, compatibility, test strength, and architecture boundaries.

Verification: the focused host-environment file passed (29 tests) with uv run --no-sync pytest; Ruff check and format-check passed for all six changed Python files; commit-message, diff, merge-tree, and the currently completed GitHub checks passed.

@LivXue

LivXue commented Oct 12, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility re-review: no new issue found in the inspected changes. One inherited Windows fixture failure remains.

Scope: the complete 6-file PR diff at head 967434832b051ce815220d4a1e24b24470e4e27a, against API base/merge base 562c8457c79063e8fe8d1eacd48882e1948f11ed. Checked the entire environment-expansion change and the POSIX/Windows documentation corrections; traced registry capture, last-good fallback, executable discovery and CLI/ACP callers. The bounded expansion, cycle handling and literal-percent regressions were included in the passing tests.

Native Windows, Python 3.13.12, isolated detached checkout and user/Raven/temp directories. Dependencies were synced with uv sync --frozen --extra dev; the reused uv environment matches this head's pyproject.toml and uv.lock. PYTHONPATH selected this checkout and its bundled plugin roots; the loaded Raven path was asserted. PYTHONUTF8=1, UV_NO_SYNC=1, UV_PROJECT_ENVIRONMENT=$R/venv. Below, $R = 'C:/Users/livxu/AppData/Local/Temp/raven-windows-review/7083af8abaec45c385fd83020f19e718'.

uv run pytest tests/test_subagent_host_env.py -k windows -n 0 -x --tb=short --basetemp "$R/checks/912-windows/pytest" --junitxml "$R/checks/912-windows/junit.xml"
# Head: 20 passed, 1 failed; 2 deselected. -x left 6 selected tests unrun.
# Run from the detached exact-base checkout, with PYTHONPATH repinned there:
uv run pytest tests/test_subagent_host_env.py::test_windows_probe_finds_agents_on_the_captured_path -n 0 -x --tb=short --basetemp "$R/checks/912-control-base/pytest" --junitxml "$R/checks/912-control-base/junit.xml"
# Base 562c8457c79063e8fe8d1eacd48882e1948f11ed: same test failed.

Reproduced on both head and exact base: test_windows_probe_finds_agents_on_the_captured_path creates extensionless executable fixtures and expects them to be found; native Windows discovery reports the first fixture as missing. It is not introduced by this PR. No machine/user registry was modified. The later unrun cases, real fresh-terminal environment and full repository suite remain outside this verification.

@0xKT

0xKT commented Oct 12, 2026

Copy link
Copy Markdown
Member

Not a blocker: full acceptance at head 9674348 merged onto main 182daca found no blocking issue. One non-blocking change is worth a look, plus two small gaps.

Severity of this one finding, not a verdict on the pull request. Any blocking findings from this pass are review threads on the changed files; GitHub renders those collapsed, as a file name with no text.

1. For a stray %, the new scan no longer expands a stored Path the way Windows does. When a % does not open a known reference, the new loop resumes at the very next character (raven/agent/subagent/backends/env.py:300-304), so the next % can open a reference. Windows does something else. In Wine's reimplementation of RtlExpandEnvironmentStrings (dlls/ntdll/env.c, the not-found branch: var = src; len++; src += len;), an unknown %NAME% is copied through its closing % and the scan resumes after it. That closing % never opens another reference, and the pass is single.

I ran main's and this head's _expand_windows side by side with a port of that loop, using USERPROFILE, LOCALAPPDATA, B, Y and Z known and A and X unknown:

stored value Windows (port) and main this PR
C:\50%;%userprofile%\bin unchanged C:\50%;C:\Users\me\bin
100%;%USERPROFILE% unchanged 100%;C:\Users\me
%USERPROFILE\go\bin;%LOCALAPPDATA%\Programs\agent (a typo) unchanged expands %LOCALAPPDATA%
%A%B% unchanged %ABval
%X%Y%Z% %X%YZv %XYvZ%

Main agreed with the port on every row, and all three agree on the plain cases (%UNKNOWN%;%USERPROFILE%\bin, %%, C:\plain%).

The module's contract is the terminal's environment ("it should see that terminal's environment rather than raven's", env.py:3-4). With such a value, a sub-agent's PATH now holds directories the user's own terminal does not have, or misses one it has. tests/test_subagent_host_env.py:291 pins the new result. If the goal is to match Windows, the not-found case should skip through the closing % as main's regex did.

This only happens on Windows hosts, and only with a literal or malformed % in a stored Path entry.

2. Small gaps:

  • The memo that the docstring says keeps a branching reference graph from multiplying the work (env.py:278-279) has no test. With if name not in resolved: replaced by if True:, tests/test_subagent_host_env.py still passes 29/29. A control mutant that drops the cycle guard fails 4 tests, so those tests do reach the loop.
  • _capture_windows's docstring still says it returns None "when neither store can be read" (env.py:220-221). The new ValueError branch (env.py:255-259) is a second way it returns None.

@gloryfromca

Copy link
Copy Markdown
Member

No blockers; suggestions only, and they are marked inline.

The new acceptance evidence changes my standing verdict from clean to nonblocking. I independently reproduced all five malformed/literal-percent cases: the current scanner resumes at the next character and expands references that the Windows-compatible scan leaves untouched. This is a concrete behavior mismatch introduced by the PR, but it requires a malformed or literal percent in a persisted Windows PATH, so it does not hold up the change under the late-round reachability bar.

The missing memoization regression test and stale None docstring are also small follow-ups, not merge blockers. A fresh focused run still passed: PYTHONPATH="$PWD/plugins-dist/everos-memory${PYTHONPATH:+:$PYTHONPATH}" uv run --no-sync pytest tests/test_subagent_host_env.py -q (29 passed).

@LivXue

LivXue commented Oct 13, 2026

Copy link
Copy Markdown
Member Author

Native Windows verification of the percent-scanning finding at 967434832b051ce815220d4a1e24b24470e4e27a: the current PR agrees with Windows on all five reported inputs. The Wine-derived result does not match this Windows host, so I have not changed the scanner.

Measured on Windows build 10.0.26300, CPython 3.13.12, in an isolated checkout with uv sync --frozen --python 3.13 --extra dev --dev. A scratch probe invoked kernel32!ExpandEnvironmentStringsW directly through ctypes.WinDLL, first querying the required buffer size, and compared its output with the imported PR _expand_windows. Variables were changed only in the probe process: USERPROFILE=C:\Users\me, LOCALAPPDATA=C:\Users\me\AppData\Local, B=Bval, Y=Yv, Z=Zv; A and X were absent.

Input Native Windows and PR output
C:\50%;%userprofile%\bin C:\50%;C:\Users\me\bin
100%;%USERPROFILE% 100%;C:\Users\me
%USERPROFILE\go\bin;%LOCALAPPDATA%\Programs\agent %USERPROFILE\go\bin;C:\Users\me\AppData\Local\Programs\agent
%A%B% %ABval
%X%Y%Z% %XYvZ%

The three controls (%UNKNOWN%;%USERPROFILE%\bin, %%, C:\plain%) also matched: 0 differences across 8 inputs.

PYTHONUTF8=1; uv run --frozen --no-sync pytest tests/test_subagent_host_env.py -k 'literal_percent or refuses_cycles or bounds_output or bounds_reference or invalid_expansion' -n 0 -x --tb=short: 9 passed, 20 deselected, no skips.

This verifies these inputs on this Windows version, not general equivalence of recursive expansion with the native single pass. The memoization coverage and docstring notes are separate follow-ups; no registry writes, full-suite run, or installed-agent launch was performed.

@gloryfromca

Copy link
Copy Markdown
Member

No blockers; suggestions only, and they are marked inline.

The direct ExpandEnvironmentStringsW measurement on native Windows is decisive for the disputed cases. I withdraw the percent-scanning mismatch concern; the Wine-derived port was not a valid oracle for this Windows behavior.

My stance remains nonblocking only for the two already-recorded minor follow-ups: add a memoization-specific regression and update _capture_windows documentation to include expansion failure as another None path. A fresh focused run passed: PYTHONPATH="$PWD/plugins-dist/everos-memory${PYTHONPATH:+:$PYTHONPATH}" uv run --no-sync pytest tests/test_subagent_host_env.py -q (29 passed).

@0xKT
0xKT merged commit 4326025 into main Oct 13, 2026
23 checks passed
@0xKT
0xKT deleted the fix/windows_environment_variable_expansion branch October 13, 2026 06:42
@LivXue

LivXue commented Oct 13, 2026

Copy link
Copy Markdown
Member Author

Both notes are fixed in #940, after the native measurement in #912 (comment).

1. A stray percent. Not changed, and the reason is that measurement: ExpandEnvironmentStringsW on Windows build 10.0.26300 returns what this PR returns for all five rows and the three controls. Wine's RtlExpandEnvironmentStrings does produce your table, so the port is faithful to Wine; it is Wine that differs from this Windows build. The scanner keeps resuming at the next character.

2a. The memo. Fixed in d5ea9b594. test_windows_expansion_resolves_each_name_once counts value reads on a branching graph (A refers to B twice, B to C twice) and asserts one read per name. With if name not in resolved: replaced by if True: the output is the same and the reads are 2, 4 and 8, so the test goes red.

2b. The docstring. Fixed in 34106e272. _capture_windows now names the second way it returns None: a stored Path that cannot be expanded (cyclic, too deeply nested or oversized).

tests/test_subagent_host_env.py: 30 passed.

0xKT pushed a commit that referenced this pull request Oct 14, 2026
…b key (#927)

## Summary

Two gaps in how Raven stops its cli sub-agents and what environment it
starts them with.

**Stopping a cli sub-agent on Windows.** A cli sub-agent's timeout,
cancel and stream-failure paths, and the `kimi` commands Raven runs to
explain a failed Kimi Code connect, ended the process group with
`os.killpg`, which Windows does not have. The `AttributeError` replaced
the timeout error or the cancellation, and the CLI was left running.
Where `killpg` is missing, the tree is now ended from the launcher's pid
with `taskkill /T /F` while the launcher is still alive, and the
launcher is then killed itself, the way `raven/ops/transport.py` already
ends a command on Windows. Once the launcher has exited its pid may name
another process, so nothing is killed by number. POSIX keeps the group
kill.

**A web key rotated through `settings.set`.** `settings.set` rewrites
`~/.raven/env` when a web credential changes, but a POSIX cli or acp
sub-agent reads that file only through the login-shell capture, which a
running Raven takes once and keeps. Every later spawn started with the
key the write had replaced, until a Connect, a Test, "Check again" or a
restart took the capture again. A mirrored key's write now takes the
capture again before the reply. Captures also run one at a time: a
refresh that read the shell before the write, a Connect's for instance,
could otherwise land after the write's own and put the replaced key
back, and readers that arrive while the first capture runs now wait for
it rather than each running the shell.

Windows is unchanged for keys. Nothing sources `~/.raven/env` there, and
a Windows sub-agent's environment is Raven's own with the registry's
PATH, so a key held only in config does not reach it; #912 states this
in the code. On POSIX the keys reach a sub-agent only through a `source`
line in the user's shell startup files, which the onboard wizard adds
only with the user's agreement, and a Windows delivery path would need a
consent step of its own, so this PR does not add one.

Not changed here: an acp sub-agent that is already connected keeps the
environment it was launched with until its next launch; a key written by
`raven onboard`, which runs as its own process, reaches a running
Raven's sub-agents at the next Connect, Test, "Check again" or restart;
and on Windows the wizard still prints the POSIX `source` line when
`$SHELL` is not bash or zsh.

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Linux, Python 3.12, rebased onto main 182daca:

- `uv run --frozen --python 3.12 --all-extras pytest -q` with the proxy
variables unset: 3 failed, 29667 passed, 158 skipped. The same 3 fail on
main 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_third_party.py tests/test_subagent_kimi_code.py
tests/test_subagent_acp.py tests/test_subagent_host_env.py
tests/test_rpc_console.py tests/test_rpc_settings.py
tests/test_rpc_subagents.py -q`: 1080 passed.
- Windows stop: `tests/_windows_kill.py` leaves a Windows host as it is,
where the real `taskkill` runs, and elsewhere removes `os.killpg` and
stands in for `taskkill` by ending the process group it is given, the
launcher's tree here; either way it records each call. The launchers,
their workers and the Kimi stand-in are Python started on
`sys._base_executable`, because a Windows venv's `python.exe` is a
redirector that runs the base interpreter as its child and returns a pid
the script never sees. The tests have not run on a Windows host, and no
CI job runs these test files there.
- Windows stop, measured here: the six tests that use the helper pass.
With `os.killpg` removed for the whole session and a `taskkill` on PATH
that ends the group it is given, they pass again through that `taskkill`
(3 calls), and the POSIX-only Kimi test beside them is skipped. With
`sys.executable` replaced by a wrapper that runs Python as its child, as
a venv's `python.exe` does on Windows, both modes stay green; started on
that wrapper instead, the two cancel tests fail on the pid they compare.
- Windows stop, against the fix: the six fail with `AttributeError` on
main. Removing the alive check in the cli backend, the `taskkill` call
or the kill after it in either module, or the cli backend's handling of
a `taskkill` that times out, turns at least one of them red. Removing
the POSIX group kill from the Kimi Code commands turns only the new test
whose stand-in `kimi` starts a process of its own red (1 failed, 347
passed over the two test files).
- Rotated key:
`test_a_rotated_key_reaches_the_capture_the_next_spawn_reads` saves a
key twice through `settings.set` and reads the capture after each save,
through a real bash that sources the mirror; on main the second read
still returns the first key. Removing the refresh turns 12 tests red;
refreshing before the mirror is rewritten turns that test red. Two
existing tests that write a web key now stub the refresh, so they do not
run the host's login shell.
- Concurrency: three tests force the interleavings with events: a
refresh that read the shell first and lands after a later one, a second
reader that arrives while the first capture runs, and readers waiting on
a first capture that fails. Each failed in 3 of 3 runs on main and
passes with the lock; removing the lock from either capture path, or
either re-check inside the first one, turns one of them red.
- With `--cov=raven --cov=raven_everos --cov-branch` added to the full
run, `python scripts/coverage_gate.py diff --base-ref origin/main
--threshold 90`: 93.22% of the changed lines (55 of 59). The four left
are except clauses for a process group or a process that is already
gone.
- `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.
- `git merge-file` of `raven/rpc/methods/console.py` with each open PR
that touches it (#883, #900, #911, #912, #923, #925), and of
`raven/agent/subagent/backends/env.py` with #912 in both orders: no
conflicts.

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

No user-facing docs describe either behavior.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

Saving a web key now waits for the user's login shell to start before
the reply, as Connect and Test already do; the capture's 15 s timeout
bounds it, and it can also wait for a capture already in flight. A
capture that fails keeps the previous one and does not fail the save. No
credential reaches a sub-agent that did not before; it is the same shell
reading the same file, read again. On Windows a stop now waits up to 5 s
for `taskkill`, and a worker that a launcher started and left behind
after exiting outlives the stop there, since Windows has no group to
address and an exited launcher's pid is not trusted. If `taskkill` is
missing, fails or does not answer within those 5 s, only the launcher is
killed, and a cli stop then waits for any worker still holding the
launcher's output pipes to exit. Revert this PR to roll back.

## Related Issues

#908, #912
0xKT pushed a commit that referenced this pull request Oct 14, 2026
## Summary

Follow-ups to the review threads left on four merged pull requests:
#909, #912, #915 and #923. Each is a defect the review named, reproduced
before any change, and repaired with a test that fails against the old
code. This also addresses the Ollama spelling issue raised in this PR's
review.

**A cache-only window lookup no longer skips a model LiteLLM answers
from its table** (from #909). The gate that keeps metadata lookups off a
model's own server skipped `get_model_info` for any id carrying an
Ollama segment, including ids LiteLLM answers offline: `ollama/Llama3`,
`litellm_proxy/ollama/llama3` and `github/ollama/llama3` were sized on
the 200000-token fallback where main read 8192. The gate now puts
LiteLLM's own precondition to LiteLLM, keyed on the caller's id, and
answers cautiously only for an id with an Ollama segment when the
installed release does not ship that check.

**A window the model's own server reported is reused inside the turn**
(the second item of #909). A caller allowed to fetch resolved a local
model's window by asking its server and threw the answer away; the next
caller, which may not fetch, answered the fallback for the same model on
every iteration, for the life of the process. The answer is now kept
under the id the request goes out under, so `ollama/`, `ollama_chat/`
and the stored `ollama-chat/` spelling share one entry. The table is
consulted before the memo, so a row LiteLLM ships still outranks a
previous ask; only a real figure is stored, so a server that was down is
asked again.

**A measurement taken while the boot verify ran is no longer
overwritten** (from #915). The boot read a row before its handshake and
wrote its verdict from that read, so a Test recorded during the
handshake was replaced by a late timeout, refusal or pass. It reads the
row again immediately before the write and leaves a newer measurement in
place. The comparison is on `measured_at_ms`, so a modes relearn does
not stop the write. Four docstrings that described the replaced rule are
corrected.

**The Windows environment capture's memo is pinned, and its docstring
names the second failure mode** (from #912).

**A key is written under the spelling the loaders read** (from #923).
Below the top level pydantic reads the camelCase alias whenever it is
present, null or not, so preferring a non-null spelling sent a write to
a key the schema ignores, and `load_config` and the live readers then
disagreed. Only the top level drops a null block; below it the first
spelling present is the one read, and every caller now states which case
it is in.

**A write aimed at a wildcard map entry reaches that entry** (the
follow-up #923's review asked for). Below the top level the spelling
helper fell back to another spelling of a key, which is right for a
schema field and wrong for a map entry: with only
`tools.mcpServers.fooBar` present, a write to
`tools.mcpServers.foo_bar.enabled` selected `fooBar`, disabling a server
the caller never named. The catalog knows which segments a path binds,
so a bound segment is now matched as written. `providers` keeps the
tolerant rule, because its runtime reader folds those spellings through
`names_same_provider`.

**Configured Ollama models reach the metadata driver under its accepted
spelling.** The wizard and model picker store Ollama ids as
`ollama-chat/<model>`, while LiteLLM expects `ollama_chat/`. The window
and output-ceiling lookups now translate that endpoint prefix using the
provider registry before consulting the candidate list. The memo and
no-fetch prefilters use the same registry normalization. The `ollama/`
completion prefix is preserved because LiteLLM keys its offline table
rows under it; non-Ollama prefixes are unchanged.

## 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

Latest review fix (Linux, Python 3.12.13, LiteLLM 1.85.0; proxy
variables unset; consumer tests rechecked after rebase onto current
main):

- New and strengthened regression cases ran before the fix: 6 failed /
108 passed, covering the memo spelling, both no-fetch metadata lookups,
the conservative guard fallback, and an id produced by `stored_model_id`
that could not reach the daemon.
- `uv run --no-sync pytest tests/test_provider_rates.py -q -n 0
--tb=short`: 114 passed after the fix.
- `uv run --no-sync pytest -q --tb=short <consumer-test-files>
--cov=raven.providers.rates --cov-branch
--cov-report=json:<coverage-json>`: 1312 passed, 1 skipped. The
selection contains 16 test modules importing `raven.providers.rates`,
plus `test_provider_registry_lookup.py` and
`test_provider_wire_model.py`. The skip is the permission test that
cannot deny a root process with `chmod 000`; existing Pydantic warnings
remain.
- Applying the repository's `parse_changed_lines` and
`calculate_diff_coverage` to the fix commit after rebase yields 100%
(13/13 executable changed lines). This is coverage of this fix, not a
new full-PR coverage or full-suite ratchet result.
- Six independent in-memory mutants were caught: raw memo prefilter, raw
endpoint guard, raw shared predicate, omitted window spelling
conversion, omitted ceiling spelling conversion, and replacing only the
endpoint conversion with `wire_model` (which loses the `ollama/` table
rows). The worktree was not rewritten by the mutation checks.
- `UV_NO_SYNC=1 make lint-python lint-imports lint-deps lint-types`:
passed after rebase; 2268 Python files formatted, 10 import contracts
kept, no dependency issues. Both commit-message validators and the
source-language and large-file gates also passed. `uv run --no-sync ty
check raven/providers/rates.py tests/test_provider_rates.py` also
passed.
- Against a loopback stand-in daemon, the actual stored
`ollama-chat/qwen2.5` id makes no request in cache-only mode before a
fetch. One allowed `/api/show` request learns 8192, after which the
cache-only lookup and `ModelBinding.context_window` both return 8192
without another request, and the output limit is 4096.
- The daemon-path parameter cases clear LiteLLM's own id-keyed cache so
alias cases cannot inherit a previous case's successful response.

Earlier author verification, before this review fix:

Commands recorded at the original PR revision and main baseline, with
proxy variables unset:

```
uv run --frozen --python 3.12 --all-extras pytest -n 4 -q
```
3 failed, 29796 passed, 158 skipped, 2 xfailed. A same-day run of the
original main control gives the same 3 failed with 29771 passed, so the
failure set is identical and the 25 extra passes are the tests added
here. The three are environment-local:
`test_resolve_node_dir_answers_each_case_it_exists_for` (npm on PATH),
`test_a_legacy_workbook_is_converted_and_read` (LibreOffice), and
`test_what_cannot_be_read_names_nothing` (load-dependent).

Focused runs on the changed files:
- `tests/test_provider_rates.py` 110 passed
- `tests/test_config_self_surface.py` `tests/test_rpc_settings.py`
`tests/test_config_update.py` `tests/test_raven_config_tool.py`
`tests/test_rpc_console.py` 805 passed
- `tests/test_subagent_host_env.py` 30 passed

Gates: `ruff check`, `ruff format --check`, `ty check`, `lint-imports`,
`deptry`, `scripts/check_commit_messages.py origin/main..HEAD`,
`scripts/check_source_language.py origin/main..HEAD` and
`scripts/check_large_files.py origin/main..HEAD` all pass. `pre-commit
run --all-files` fails two hooks on three files this branch does not
touch; the same three fail the same way at the base, so they are
pre-existing.

Every new test was checked by reverting the fix: six single-construct
mutants over the two repaired rules, each restored and compared after
running. Two survived the first pass and both were findings rather than
tests to tighten. The guard that refused a zeroed window was unreachable
(the one caller already has a window), so the parameter is required
instead of optional. And the memo originally ranked above LiteLLM's own
table, so a previous ask could have overridden a row a caller could have
read offline; the table now comes first.

Reproduced end to end against a stand-in server on a loopback port: one
`/api/show` request answers 8192, `ollama/qwen2.5` read with
`allow_fetch=False` answers 8192 with no second request, and
`ModelBinding.context_window` on that model answers 8192 where it read
the fallback before.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

No stored config changes shape or meaning. The alias-spelling fix
changes where a write lands when a file carries both spellings of one
key, which is the file that did not load correctly before. The wildcard
fix changes where a write lands when two map entries differ only by
spelling; both are left as written, where the old code silently edited
the neighbour.

The window memo is process-wide and holds context-window and optional
output-limit counts per model, cleared by the same
`reset_openrouter_cache` the catalogue uses. Stored `ollama-chat/` ids
can now make a metadata request to the configured daemon when fetch is
allowed; cache-only callers continue to reuse local data. Other
hyphenated provider prefixes are outside this fix. Roll back by
reverting the merged change; the memo is not persisted.

## Related Issues

#909, #912, #915, #923
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