Skip to content

feat(*): read, write and deliver files on a registered machine by address - #922

Merged
Handsome-wzw merged 33 commits into
EverMind-AI:mainfrom
Handsome-wzw:feat/subagent_server_files
Oct 14, 2026
Merged

Handsome-wzw merged 33 commits into
EverMind-AI:mainfrom
Handsome-wzw:feat/subagent_server_files

Conversation

@Handsome-wzw

@Handsome-wzw Handsome-wzw commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Third of four changes that let a Raven dispatch work to the machines in
its machine registry. #920 is merged; this branch is opened against main
ahead of #921, so the diff here includes #921's commits as well: review
from "read, write and deliver files on a registered machine by address"
on (the commits before it are #921's, carried here until it merges).

What this does:

  • The file tools take a path on a registered machine as it is written
    for scp -- user@host:/path, or scp://user@host:port//path when the
    port is not 22 -- and answer it through the machine's daemon
    (raven/agent/tools/remote_files.py, hooked from filesystem.py and
    file_search.py): read_file, list_dir, find and grep run
    Raven's own tools there and bring back the answer, so a read of fifty
    lines of a large file moves those fifty lines; write_file and
    edit_file write there, with the digest of that agent's own last read
    -- the conversation's, or its sub-agent run's -- as the expected
    state, so a file changed since it read it is refused and nothing is
    written, and another agent's read does not stand in for its own. The
    path is resolved against the registry
    only, never this computer's ssh config, so one path names one machine
    on any computer; one that matches no row is refused, not guessed.
  • What is shown back as an address is only what the tool says about
    paths -- its errors, its headers and footers, a write's account, a
    diff's two header lines; a read's numbered lines, a search's matches
    and context, and a diff's body come back as they are, so the model
    edits against the bytes the file holds. The shell's refusal of typed
    ssh to a registered machine (raven/agent/tools/shell.py,
    machine_exec.py) reads the program in any spelling -- a path, a
    backslash, and on a Windows host ssh.exe, SSH.EXE or a whole
    C:\...\ssh.exe, with the backslashes the shell there keeps and
    without the quotes it takes off (a quoted piece and a bare one next to
    it making one word), so a quoted destination or port is matched like a
    bare one -- and, with the lane on, names the file tools. scp, sftp
    and rsync to a registered machine stay what they are on main: a copy
    starts nothing there and passes exec's own question like any command,
    and the file tools cannot carry a directory, a binary file or a large
    one, so the copy stays the route for those. A drive-relative or
    extended Windows path (C:file, \\?\C:\..., UNC) is this
    computer's, not a machine nobody registered.
  • Permissions (raven/permissions/): a path inside the directories
    Raven's agents work in on that machine -- its session directory, its
    remoteCwd, the row's listed paths -- is read without asking;
    elsewhere a read asks once per machine and directory for the session.
    A write there asks wherever it falls, as a write here does, and its
    grant is for the one file, apart from any read. The daemon checks the
    roots again on the machine, links included.
  • deliver_files fetches one file by its address, whole and checked,
    under a 20 MB cap, into <workspace>/pulled/<machine>/<key>/<name>,
    the key a digest of the file's path on the machine, so two paths never
    share a landing: not two files of one name from two directories, and
    not two names this computer's filesystem reads as one (Report.txt
    and report.txt, a trailing dot). The name beside the key is written
    as one this computer keeps (a backslash, what Windows reserves in a
    name, control characters and the percent sign itself as
    percent-codes), and a landing that would not resolve under the
    machine's delivery directory is refused before anything is made; a
    bigger file is named with its size and read there instead. Only a file under the directories this Raven's agents work in
    on that machine is fetched: the tool asks nothing, and a read some
    conversation was allowed is no evidence for this one, so a file
    elsewhere is refused and read there instead. Nothing reclaims
    pulled/: a delivery is opened from its landing for as long as its
    record lasts, and a bound by size or age is left out of this change on
    purpose.

Type

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

Verification

  • uv run pytest tests/test_remote_file_tools.py tests/test_permissions_remote_read.py tests/test_deliver_files_tool.py tests/test_shell_machine_channel.py tests/test_ops_remote_path.py tests/test_permissions_gate.py tests/test_import_cycle_budget.py tests/test_node_server.py tests/test_node_client.py tests/test_subagent_acp_remote.py tests/test_acp_remote.py tests/test_cli_ops_connection_commands.py
    -- all passed on this tree (703 in the run that covered the first
    eight groups, the node's own suite and the real-node cases among them).
    Covered: each tool on a remote path answers as its local tool would,
    through a stood-in link and a real daemon; a path outside the roots is
    refused on both sides; a write with a stale digest is refused and
    writes nothing; the address form on what the tool says about paths,
    and a read's lines, a grep's matches and a diff's body untouched; the
    shell's refusal of typed ssh to a registered machine, by host, by
    alias, in a pipeline, as ssh.exe and SSH.EXE, and with a whole
    Windows path or a quoted destination or port when the host is Windows,
    a copy with scp, sftp or rsync not refused, and the file tools named
    only with the lane on; a fetched name holding the
    other platform's separators lands as one name under the machine's
    directory and nothing lands elsewhere, and a landing that would
    resolve outside fetches nothing; a Windows path form read here with the lane on; the read
    permission inside the roots and the question elsewhere, remembered per
    machine and directory, a write asked wherever it falls and its grant
    kept per file and apart from a read; a read's digest kept per agent,
    so another agent's read lets no stale overwrite through and an agent
    that never read the file expects nothing; a delivery fetched whole
    under a key of its path (two files of one name, two names a
    filesystem reads as one, and the percent-code pair all land apart),
    refused over the cap and refused outside the roots whatever was read
    before.

  • uv run pytest on this tree after the merge with main -- 31210 passed,
    231 skipped, 2 xfailed, 4 failed, every one of the four in a test that
    needs what this machine lacks (test_rpc_files.py, the two simulation
    cases and the kimi-code case that fail the same way on main here); the
    groups above also pass under --idle-ceiling-strict, CI's idle ceiling.

  • Diff coverage of this change against main after the merge: 95.27% (805 of
    845 executable changed lines), against the 90% gate. The node suite,
    the remote-path tests and the real-node cases among the groups above run
    on the merged tree.

  • Real machines (two registered Linux machines): fifty lines from the
    middle of a 5 MB file read through the daemon in 30 to 35 ms, byte for
    byte what this computer reads; list, find and grep answered; a write
    and an edit landed as the local tools would, and a write after the file
    changed was refused; a path outside the roots refused; a file written
    by an agent there fetched back by address. The shell's refusals are
    covered by the tests above.

  • scripts/check_source_language.py, scripts/check_commit_messages.py
    and scripts/check_large_files.py on the branch range, commitlint,
    ruff -- all clean.

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

User docs follow once the lane is no longer debug-only.

Risk

Behind the debug lane, as the two changes before it. With it on, a remote
path reaches the machine only through its daemon, inside the roots each
call names and under the read and write questions above; the shell cannot
run a command on a registered machine by address, and a copy to or from it
with scp, sftp or rsync is the plain shell's behind exec's own question, as
on main. A write refused for a stale
digest writes nothing. A delivery fetches at most 20 MB per file into the
workspace, and only from the directories the agents work in on the
machine. Rollback is a plain revert.

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

Related Issues

#920 (merged); depends on #921.

Handsome-wzw and others added 3 commits October 12, 2026 19:03
…ched ssh link

A registered machine gets this Raven's own code as a daemon (raven/node):
started by the relay the host's ssh runs there (`python -I -m raven.node
--attach --dir <node_dir>`) when none answers on its socket under the
node directory, and left on its own once idle. It answers file calls with
Raven's own tools fenced to the directories each call names (fs/read,
list, find, grep, stat, write, edit, pull), starts acp agents under task
ids of the host's in their own process groups in the machine user's
login environment, keeps each agent's frames in the task's record on
disk so a connection that goes leaves the agent running and the next one
takes it over from the frame the host last read (agent/attach), lists
the tasks it runs or remembers, and probes the machine and an agent's
command. It opens no network port; only --stdio ties the agents to the
connection. `raven ops connection install-node <id>` puts it on a
machine after one look and one question, and `--prune` removes older
installs.

The host keeps one link per machine (raven/node/client.py): the ssh line
runs the relay, a heartbeat watches the link, a lost link parks the
agents on it and is tried again on a backoff for as long as any waits,
and a link that comes back attaches every task again by its id, sending
what the host wrote meanwhile and any cancel asked in between. The
registry's package tells a remote path from a local one and lends the
tools the pool through one seam (raven/ops/remote_path.py); the machine
lane stays debug-only (RAVEN_DEBUG_MACHINES=1).

First of three changes on this design; the next runs acp sub-agents on
a machine through this daemon, the one after reads and writes files
there by address. Nothing dispatches to a machine yet.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…mon, add it from a chat

An acp entry that names a `machine` (and a `remoteCwd`, `~/raven-work`
by default) is started there by the machine's Raven daemon, over the
link the host keeps to it: its command runs in the machine user's login
environment with the entry's env on the link, never on a command line;
its session works in `<remoteCwd>/<instance or task id>`, made there for
each dispatch; its ACP frames travel on a channel of the link, under a
task id, so an ssh that goes mid-turn leaves it running and the turn goes
on once the link is back; a cancel ends its whole process tree. The
reply says where it ran, by address, and that what it wrote is there.
The files that come with a task are named to the agent by address
(remote.files_note); nothing is carried over. Raven's keys are not lent
to such an entry, and the lane stays debug-only: with it off the entry
is left off the table and every door says so in one sentence.

The roster names the machine after the entry's tags; the free probe
reads the link's state and the last test, never the machine; a test
runs the real handshake there and marks a refusal "needs attention"
without offering a command to run here. `subagents.add` takes `machine`
and `remote_cwd` for an acp preset -- from the page, and from a chat
through `raven_config`, which lists the registered machines while the
lane is on -- names the row `<preset>@<machine>`, refuses before anything
is spent when the lane is off, the id is not usable, the preset is not
acp or a key would be lent, and proves the entry with the same readiness
ping as a local add, run on the machine. The list rows carry `machine`;
the contract and the generated clients carry the fields.

Second of three changes on this design; the one after reads and writes
files on a machine by address.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…ress

The file tools take a path on a registered machine as it is written for
scp -- `user@host:/path` or `scp://user@host:port//path` -- and answer it
through the machine's daemon: read_file, list_dir, find and grep run
Raven's own tools there and bring back the answer; write_file and
edit_file write there with the digest of the last read as the expected
state, so a file changed since is refused and nothing is written;
deliver_files fetches one file by its address, whole and checked, under
a size cap. A path is read inside the directories Raven's agents work in
on that machine without asking, elsewhere once per machine and
directory for the session, writes asked apart from reads. What is shown
back is the address form, never a bare address; and the shell refuses
ssh, scp, sftp and rsync to a registered machine, naming the file tools.

Third of three changes on this design.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
@Handsome-wzw
Handsome-wzw requested a review from LivXue as a code owner October 12, 2026 11:14

@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: remote file reads and deliveries need to preserve content and permission isolation, and node installation must comply with the repository's uv-only rule.

I found four blocking issues, marked inline.

Review coverage: I read the full github/main...HEAD diff, AGENTS.md/CLAUDE.md, CONTEXT-MAP.md and the Runtime/UI context files; inspected the stacked commit history, surrounding callers, remote-node lifecycle, permission flow, delivery store, RPC/schema changes, and compatibility behavior with the debug lane disabled. I also checked that tests were added rather than weakened and reviewed the declared package-layer constraint.

Verification:

  • uv run pytest tests/test_node_bundle.py tests/test_node_client.py tests/test_node_install.py tests/test_node_relay.py tests/test_node_server.py tests/test_ops_remote_path.py tests/test_remote_file_tools.py tests/test_permissions_remote_read.py tests/test_deliver_files_tool.py tests/test_cli_ops_connection_commands.py tests/test_acp_client_teardown.py tests/test_acp_remote.py tests/test_rpc_subagents.py tests/test_shell_machine_channel.py tests/test_subagent_acp.py tests/test_subagent_acp_remote.py tests/test_subagent_probe.py tests/test_ops_connection_add.py tests/test_ops_transport.py tests/test_raven_config_tool.py -x - 1,338 passed, 4 skipped (csh/tcsh unavailable).
  • uv run pytest tests/test_import_cycle_budget.py tests/test_rpc_schema_match.py tests/test_rpc_import_sync.py tests/test_config_schema.py -x - 495 passed.
  • scripts/check_large_files.py, scripts/check_source_language.py, and scripts/check_commit_messages.py via uv run --frozen --extra dev - passed.

The first test attempt stopped in the autouse fixture because the fresh environment lacked the declared everos-memory dev extra; after uv sync --frozen --extra dev --dev, the same suite passed as reported above.

Comment thread raven/agent/tools/remote_files.py
Comment thread raven/agent/tools/deliver.py Outdated
Comment thread raven/agent/tools/deliver.py Outdated
Comment thread raven/node/install.py Outdated
@LivXue

LivXue commented Oct 12, 2026

Copy link
Copy Markdown
Member

Windows compatibility review: two additional findings in the file-tool layer, plus the shared Windows node-launch failure.

Scope: complete 69-file PR diff at f67f806ed950469b47d72d2175ffd4be59fd0dca, API base 82c76a82a9a7d312ef6cd75e560404bccd81d292, merge base 829eb28e1db877dffdd3df4f835d707e1f049d3a, including source, callers, tests and surrounding platform guards. Inspected isolated exact-SHA source archives; all changed-file blob hashes and all patch addition/deletion counts matched GitHub.

  1. P2 - Recognize Windows executable names in the copy refusal. Reproduced with execution intercepted. machine_exec.py:370 compares a POSIX basename literally against scp/sftp/rsync. For the same registered destination, scp -P 58717 ... is refused by ExecTool, but scp.exe, SCP.EXE and C:\Windows\System32\OpenSSH\scp.exe all reach its executor. Once an exec call is admitted, these normal Windows spellings bypass the new file-tool budget/read checks. Normalize the local executable with Windows path, case and suffix rules, and parse it consistently with the executing shell. No copy or network connection was performed.

  2. P2 - Keep drive-relative and extended Windows paths local. Reproduced against the base. remote_path.py:330 only excludes the simple C:/ or C:\ drive forms. With RAVEN_DEBUG_MACHINES=1, the new read_file dispatch rejects both C:review_marker.txt and \\?\C:\...\review_marker.txt as unknown remote machines. Both read the actual scratch file on the exact API base, and on this head with the lane disabled; an ordinary absolute drive path works in every case. Recognize native Windows drive-relative and extended path forms before SCP detection, and cover them through the public file tools.

  3. P2 - Shared node SSH argv failure, reproduced here. remote.py:188 is identical to feat(*): a Raven daemon on a registered machine, reached over one watched ssh link #920: the real node command becomes 12 Windows arguments instead of one, preventing the file tools from opening their link. Apply the shared correction described in #920's Windows review. The identical POSIX-only test imports/teardown fixtures also retain feat(*): a Raven daemon on a registered machine, reached over one watched ssh link #920's measured Windows collection and SIGKILL failures (code inspection here).

Native Windows / Python 3.13.12, PYTHONUTF8=1, UV_NO_SYNC=1, isolated user/Raven/temp directories. A pre-synced uv environment supplied the unchanged locked dependencies; imports were asserted to come from each inspected snapshot. Commands ran from the named snapshot root.

uv run python 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\probe_windows.py' copy 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\pr922\copy-workspace'
# scp refused; all three Windows .exe spellings reached the intercepted executor
uv run python 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\probe_windows.py' paths 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\pr922\paths-workspace'
# lane on: drive-relative and extended paths refused; ordinary absolute path read
uv run python 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\probe_windows.py' node-ssh
# 12 remote-command arguments instead of 1
uv run pytest 'tests/test_remote_file_tools.py' 'tests/test_permissions_remote_read.py' -n 0 -x --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\pr922\file-tests-pytest'
# 38 passed, 3 skipped (POSIX-shell fixtures)
uv run pytest 'tests/test_deliver_files_tool.py' -k 'machine or budget or what_was_not_delivered' -n 0 -x --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\pr922\delivery-tests-pytest'
# 3 passed, 23 deselected
uv run pytest 'tests/test_shell_machine_channel.py' -k 'copy or typed_ssh' -n 0 -x --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\pr922\shell-tests-pytest'
# 29 passed, 94 deselected

Exact base control, from its snapshot root:

uv run python 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\probe_windows.py' paths 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\base\paths-workspace'
# all three path forms read the scratch file, with the lane both on and off

No live SSH/remote filesystem, real transfer, remote install or full suite was exercised. The existing content-rewriting, filename-collision, cross-session approval and installer findings remain separate.

Handsome-wzw and others added 5 commits October 13, 2026 01:18
…when the machine has none

The install ran `python -m venv` and `python -m pip install` on the
machine, against the repository's rule that uv is the only Python package
manager. Everything Python now goes through uv: `uv venv` of the machine's
own Python 3.12 or of one uv keeps inside the node directory, `uv pip
install` for the pinned packages and the optional search package. A
machine without uv gets one under `<node_dir>/.uv` first, from uv's own
installer fetched with the machine's curl or wget and told to touch
nothing else; the probe looks for curl and wget, and for a uv an earlier
install put there; with no uv and no way to fetch one the install stops
and says so. The plan counts the room uv takes and says when it is fetched.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…s stay off the network

The reply of a run on a machine pointed at read_file, list_dir, find,
grep, write_file, edit_file and deliver_files for the files it left
there, which on this change still read a path as this computer's: it
now says only where the files are. Two tests of an add on a machine
reached for the machine over a real ssh when the capability record ran
after the stood-in ping, and waited out its connect timeout on CI: the
record is stood in for too. The page's fixture gate lists the `machine`
field of a sub-agent row as one its library does not send (every row
there runs on this computer), and a daemon the stand-in ssh starts in a
test leaves soon after the test does.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
… file under its whole path

A read's numbered lines, a search's matches and a diff's body came back
with every registered root rewritten as an address, so the model edited
against text the file does not hold: only the tool's own sentences --
its errors, its headers and footers, a write's account and a diff's two
header lines -- are shown that way now. A delivered file landed under
its bare name, so two files of one name from two directories landed on
each other: it lands under its whole path on the machine. A file
outside the directories this Raven's agents work in there was fetched
on the strength of a read some conversation had been allowed, which is
no evidence for this one: it is refused, and read there instead. The
remote run's reply names the file tools again, now that they take its
path.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…h argv goes as it is

The socket's fallback for a long node directory lay in the temp
directory itself, so the relay forwarded the host's lines to whatever
listened at that path, and a file there the user could not remove kept
the daemon from starting: the fallback now lies in a directory of the
user's own (0700, checked, never a link or another's), the relay
forwards only to a socket of the user's, and a daemon refused its
socket's place says so in one line and leaves. The task records -- the
agent's frames as they were -- were made with the default mode: tasks/,
each record and its three files are the owner's alone now, as the
host's journal is, and the installer makes the node directory so. A
page read, a digest, a chunk and a checked write ran on the daemon's
one loop, which also answers the heartbeat: they run in a thread now,
writers one at a time under a lock. The ssh argv went to the connection
as one shell-quoted line, which a Windows host's own grammar split into
twelve words: it goes as the argv, which no host reads again. A call on
its way to a node counts as use, so a link is not closed under it as
idle. A drive-relative or extended Windows path (C:file, \\?\C:\...)
no longer reads as a machine's. The daemon's tests skip where there is
no fcntl, the POSIX teardown tests lend a SIGKILL where the platform
has none, the login-shell test checks a directory of its own instead
of /nonexistent, a node is run with nothing importable beyond its
requirements, and remote.py says what does reach a model: a path on
the machine, written the way scp writes it.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…dows path form to this computer

The copy refusal compared a POSIX basename with scp, sftp and rsync, so
scp.exe, SCP.EXE and C:\Windows\System32\OpenSSH\scp.exe reached the
executor and went around the file tools' budgets and read check: the
program a word runs is read with the directory, the case and the suffix
taken off, and on a Windows host the words are read with their
backslashes kept, as the shell there reads them. A drive-relative or
extended Windows path (C:file, \\?\C:\...) was refused by read_file as
a machine nobody registered: it is this computer's, through the public
tools too.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
@Handsome-wzw

Copy link
Copy Markdown
Contributor Author

All three are in the commits now on this branch. The copy and ssh refusals read the program with the directory, the case and a .exe/.cmd/.bat suffix taken off, and on a Windows host the words are read with their backslashes kept, as the shell there reads them, so scp.exe, SCP.EXE and a whole C:\...\scp.exe are refused like scp (tests, with the host stood in as Windows for the whole-path spellings). A drive-relative path (C:file, on a Windows host) and an extended or UNC path (\\?\C:\..., \\server\share) are this computer's, through read_file with the lane on (test). The ssh argv fix is in the #920 commit this branch carries.

…test sets its own mode

The record opener handed os.fdopen a splatted keyword map, which no
overload of it matches under the type check: the binary and the text
mode are two calls now. The test of another user's directory asserted
the mode mkdir leaves, which is the umask's (0775 under 0002, where it
failed): the test sets 0755 first and asserts it is left so.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>

@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 complete github/main...HEAD change and the final two-file delta at 3b790620c54a. I covered AGENTS.md/CLAUDE.md and CONTEXT-MAP/CONTEXT rules, the diff, direct callers and history, backward compatibility, test-strength changes, and the enforced layer architecture. I also traced the remote-file authorization, path fencing/presentation, collision handling, SSH argv, node socket/record ownership, protocol framing, and Windows path/copy boundaries.

The four earlier blockers are fixed: content is no longer rewritten, fetched paths are collision-free, delivery does not reuse another sessions read evidence, and installation is uv-only. The Windows executable/path/SSH-argv follow-ups are also covered by the final code and regression tests. I found no new blocker or plain error.

Verification:

  • uv run --frozen --python 3.12 --all-extras pytest -q: 29,208 passed, 132 skipped, 1 failed. The sole failure is the pre-existing untouched tests/test_cli_theme.py::test_bold_accent_renders_styled_not_bare ANSI/theme assertion; neither that test, theme code, Rich dependency, nor lockfile changes in this PR.
  • uv run pytest tests/test_node_server.py -q: 90 passed.
  • uv run --frozen --python 3.12 --all-extras ty check raven evolver agents plugins-dist scripts: passed.
  • uv run --frozen --python 3.12 lint-imports: 10 contracts kept, 0 broken.
  • scripts/check_source_language.py github/main..HEAD: passed.

@LivXue

LivXue commented Oct 12, 2026

Copy link
Copy Markdown
Member

Windows compatibility re-review: two runtime regressions reproduced, including a write outside the workspace, plus the shared path-test failure.

Scope: all 71 changed files at 3b790620c54a4828b1067cc957e961668e2591c7; API base 82c76a82a9a7d312ef6cd75e560404bccd81d292, merge base 829eb28e1db877dffdd3df4f835d707e1f049d3a. Rechecked the complete PR scope and revision delta, using blob identity for unchanged previously reviewed files and tracing the affected callers.

  1. P1 - Keep fetched remote filenames inside the local delivery directory. Reproduced through deliver_files. deliver.py:100 now appends Path(place).relative_to("/") to the local destination. On a Linux machine, backslashes are legal filename characters; the POSIX root check therefore accepts a filename containing backslash-separated .. text beneath /root/raven-work/. Windows Path reinterprets those characters as separators and parent traversal. With a stubbed node returning five bytes, the real public tool wrote and registered review_escape.txt in the parent of its workspace. All writes were confined to an owned temporary directory. Parse the remote name as POSIX, encode components that are unsafe on Windows, and verify the resolved destination remains under the intended pulled root before creating directories or writing bytes. The prior reviewed head's basename mapping kept the identical input inside its workspace; this regression comes from the new whole-path mapping.

  2. P2 - Decode quoted destination and option tokens before matching the registered machine. Reproduced with execution intercepted. machine_exec.py:209 and line 384 use shlex(..., posix=False), which retains the quotes. Only the executable name is unquoted. Thus ssh -p 58717 "root@203.0.113.7" true matches a host with a trailing quote, and scp -P "58717" a.txt root@203.0.113.7:/tmp/a falls back to port 22 because the quoted port fails isdigit(). Four quoted ssh/scp/sftp variants reached the intercepted executor, while their unquoted controls were refused. Normalize all relevant argv tokens consistently with the executing Windows shell before host/port matching. Both quoted and unquoted SSH were refused on the exact API base; all four variants were also refused on the previous reviewed head.

  3. P2 - Update the shared a:b.txt test expectation. Reproduced. test_ops_remote_path.py:101 expects an unknown-machine error for what the corrected Windows parser treats as a local drive-relative path. Use a multi-character unknown host or a platform-specific expectation, as in the shared feat(*): a Raven daemon on a registered machine, reached over one watched ssh link #920 finding.

Native Windows / Python 3.13.12. Git fetching failed; the isolated fixed-SHA snapshots were reconstructed and every blob plus the complete Git tree was verified. A fresh environment was created with uv sync --frozen --python 3.13 --extra dev after the first environment-reuse attempt failed before tests ran. Tests used isolated user/Raven/temp/registry paths, PYTHONUTF8=1, UV_NO_SYNC=1, UV_PYTHON=3.13, and UV_PROJECT_ENVIRONMENT=$R/venv; PYTHONPATH selected the tested snapshot and plugin roots, with the loaded source asserted. $R = 'C:/Users/livxu/AppData/Local/Temp/raven-windows-review/2d5904b4d92f4f238e9db27e2ade2ddf'.

uv run pytest tests/test_remote_file_tools.py tests/test_permissions_remote_read.py tests/test_deliver_files_tool.py -n 0 -x --tb=short --basetemp "$R/checks/922-final-files/pytest" --junitxml "$R/checks/922-final-files/junit.xml"
# 70 passed, 3 POSIX-shell skips.
uv run pytest C:/Users/livxu/AppData/Local/Temp/raven-windows-review/2d5904b4d92f4f238e9db27e2ade2ddf/test_windows_resolved_paths.py -s tests/test_acp_client_launch.py tests/test_acp_client_teardown.py -n 0 -x --tb=short --basetemp "$R/checks/922-final-native/pytest" --junitxml "$R/checks/922-final-native/junit.xml"
# 9 passed: one SSH command argument, real local path reads, five executable spellings refused, ACP argv/teardown.
uv run pytest tests/test_ops_remote_path.py -k path_that_names_no_registered_machine -n 0 -x --tb=short --basetemp "$R/checks/922-final-path/pytest" --junitxml "$R/checks/922-final-path/junit.xml"
# 11 passed, 1 failed (a:b.txt), 79 deselected.
uv run pytest C:/Users/livxu/AppData/Local/Temp/raven-windows-review/2d5904b4d92f4f238e9db27e2ade2ddf/test_windows_review_probes.py -k 'not exact_base' -s -n 0 -x --tb=short --basetemp "$R/checks/922-final-probes/pytest" --junitxml "$R/checks/922-final-probes/junit.xml"
# 2 probes passed by confirming the two defects above; 1 base-only case deselected.
# Prior reviewed head f67f806ed950469b47d72d2175ffd4be59fd0dca, imports repinned there:
# REVIEW_EXPECT_DEFECT=0
uv run pytest C:/Users/livxu/AppData/Local/Temp/raven-windows-review/2d5904b4d92f4f238e9db27e2ade2ddf/test_windows_review_probes.py -s -n 0 -x --tb=short --basetemp "$R/checks/922-prior-controls/pytest" --junitxml "$R/checks/922-prior-controls/junit.xml"
# 2 controls passed: no quoted-command bypass and no file outside the workspace.
# Exact API base 82c76a82a9a7d312ef6cd75e560404bccd81d292, imports repinned there:
uv run pytest C:/Users/livxu/AppData/Local/Temp/raven-windows-review/2d5904b4d92f4f238e9db27e2ade2ddf/test_base_windows_ssh.py -s -n 0 -x --tb=short --basetemp "$R/checks/922-base-ssh-checked/pytest" --junitxml "$R/checks/922-base-ssh-checked/junit.xml"
# 1 passed: quoted and unquoted SSH both refused.

The API base has no remote-delivery implementation. The file-transfer probe uses a stub node pool with real local filesystem writes; the command probe replaces execution entirely. No remote connection or transfer occurred. A shell-suite run on the preceding head also stopped after 7 passes at a quoted rsync-option assertion; its parser is unchanged in the final two-file POSIX daemon delta. An initial base-probe collection attempt imported a module absent from that base; the standalone base control above corrected the probe.

The previously reported bare .exe spellings, drive/extended-path misclassification and node argv splitting passed their focused rechecks. No live daemon/agent/installer, full suite, or Windows ACL/junction stress was run.

@gloryfromca

Copy link
Copy Markdown
Member

Blocking: Windows remote delivery can escape the pulled directory, and quoted Windows SSH/copy arguments bypass registered-machine enforcement.

The new Windows reproduction changes my standing clean verdict on this unchanged revision. I independently confirmed the reported mechanisms without performing a connection or transfer:

  • A remote POSIX filename may legally contain backslashes and repeated .. text. Path(place).relative_to("/") reparses those characters with Windows semantics, and enough such components normalize the destination beyond <workspace>/pulled/<machine> before pull_file creates and replaces the local file.
  • With the Windows parser branch enabled, ssh -p 58717 "root@203.0.113.7" true produces host 203.0.113.7", while scp -P "58717" ... falls back to port 22. Both therefore evade a registry match that their ordinary quoted Windows command lines should trigger.
  • The a:b.txt expectation is consequently platform-inconsistent after drive-relative paths were made local on Windows.

uv run pytest tests/test_deliver_files_tool.py tests/test_shell_machine_channel.py tests/test_ops_remote_path.py -q still passes on Linux (256 passed), which does not exercise or refute these Windows paths. The Windows findings in comment 6130778655 remain outstanding and need correction before merge.

Handsome-wzw and others added 2 commits October 13, 2026 03:09
…host reads as a drive

On a Windows host one letter before the colon is a drive, so `a:b.txt`
is this computer's there and the test that expects a refusal failed
(reviewed 2026-10-12); a two-letter name reads as a machine's everywhere.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…ed word reads bare on Windows

A fetched file landed under its path on the machine joined as this
computer's path, and a Linux file name may hold backslashes: on a
Windows host `..\` inside one name read as two steps up, and the file
landed above the workspace (reviewed 2026-10-12). The path is read as
POSIX now, each component written as one name here (the other
platform's separator, what Windows reserves and control characters as
their percent-codes), and a landing that would not resolve under the
machine's delivery directory is refused before anything is made. On a
Windows host the words of a command kept their quotes, so a quoted
destination or port went unmatched and the command reached the
executor: the quotes that shell takes off are taken off before the
match.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
@Handsome-wzw

Copy link
Copy Markdown
Contributor Author

All three are on the branch now (a010ad8 for the two runtime ones, 7b3757c for the test).

  1. The landing place of a fetched file is built from the path's POSIX components, each written as one name on this computer -- a backslash, what Windows reserves in a name (<>:"|?*) and control characters become their percent-codes, . and .. too -- and the resolved landing is checked to lie under pulled/<machine>/ before any directory is made or byte written; one that would not is refused in a sentence (_local_name, _landing in deliver.py; tests: a name holding ..\..\..\ lands as one name under the machine's directory with nothing elsewhere, and a landing that resolves outside fetches nothing).
  2. On a Windows host every word of the command loses the quotes that shell takes off -- a pair around it, or double quotes inside it -- before the host and port are matched (_words, _unquoted in machine_exec.py), so "root@host", -P "58717", -p"58717" and a quoted rsync -e are read like their bare forms (tests with the host stood in as Windows: all refused, nothing run; the words checked directly).
  3. The colon-in-a-name case names a two-letter host (ab:b.txt), which reads as a machine's on every host.

@gloryfromca

Copy link
Copy Markdown
Member

No blockers; this can merge as far as I am concerned.

Confirmed at a010ad8d2e2e: fetched remote paths are now decomposed as POSIX paths, encoded component-by-component, and containment-checked before any filesystem mutation; quoted Windows SSH/SCP/SFTP/rsync arguments are normalized before registered-machine enforcement; and the corrected drive-relative test uses an actually ambiguous Windows spelling. The focused regressions pass.

@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 exact 3b790620c54a...a010ad8d2e2e delta, the affected delivery and command-parsing callers, commit history, backward compatibility, and the regression changes. I also rechecked the repository rules, runtime terminology, import architecture, and that tests were strengthened rather than weakened. The prior Windows path-escape and quoted-command bypass mechanisms are closed.

Verification: uv run pytest tests/test_deliver_files_tool.py tests/test_shell_machine_channel.py tests/test_ops_remote_path.py -q (264 passed); targeted ty checks passed; all 10 import contracts were kept; source-language and git diff --check checks passed. All review threads I opened remain resolved.

@LivXue

LivXue commented Oct 12, 2026

Copy link
Copy Markdown
Member

Windows compatibility re-review: the original traversal and whole-word quoting reproductions now pass; two boundary cases remain.

Scope: the complete 71-file PR at a010ad8d2e2e6ce3a899844ab89884058b14388b, API base 82c76a82a9a7d312ef6cd75e560404bccd81d292, merge base 829eb28e1db877dffdd3df4f835d707e1f049d3a. All 4,379 snapshot blobs matched the head tree. Rechecked the five-file revision delta, callers, full PR scope and existing discussions, retaining blob-identity evidence for unchanged previously reviewed files.

  1. P2 - Make the local filename encoding collision-free. Reproduced. _local_name, deliver.py:134 encodes a backslash as %5C but leaves literal percent signs unchanged. The distinct POSIX files /root/raven-work/a\b.txt and /root/raven-work/a%5Cb.txt therefore land at the same Windows path. Calling the real deliver_files with both paths, with only the remote pool replaced, performed two writes and returned one delivery containing the second file's bytes; the first registration retained the first file's size. The identical probe on predecessor 3b790620c54a4828b1067cc957e961668e2591c7 returned two files with their correct bytes. This encoding regression is new in this revision and extends the earlier collision discussion with a different trigger. Escape the escape character too, or use a collision-resistant key derived from the remote path, while retaining the containment check.

  2. P2 - Preserve adjacent quoted/unquoted segments when tokenizing Windows commands. Reproduced. _words, machine_exec.py:124 still uses shlex(posix=False), which splits "root@"203.0.113.7 into two words and "587"17 into two words. Removing quotes afterward cannot restore their boundaries. Both ssh -p 58717 "root@"203.0.113.7 true and ssh -p "587"17 root@203.0.113.7 true reached an intercepted executor despite that host/port being registered. A harmless argv-printing child launched through native cmd.exe received -p 58717 root@203.0.113.7 true in both cases; the exact API-base ExecTool refused both. Use Windows tokenization that preserves adjacent segments and backslashes, and test against native argv. This is new evidence for the earlier quoting finding; its original whole-word quoted variants are now correctly refused.

Native Windows / Python 3.13.12, isolated user/Raven/temp directories, run-owned uv environment, UV_NO_SYNC=1, PYTHONUTF8=1; imports checked against each snapshot. Exact commands from the current head:

$R = 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\abf44f96254d43bc81b1be33079fb26b'
uv run pytest tests/test_deliver_files_tool.py -n 0 -x --tb=short --basetemp "$R\checks\922-delivery\pytest" --junitxml "$R\checks\922-delivery\junit.xml"
# 29 passed.
uv run pytest tests/test_ops_remote_path.py -n 0 -x --tb=short --basetemp "$R\checks\922-paths\pytest" --junitxml "$R\checks\922-paths\junit.xml"
# 91 passed.
uv run pytest tests/test_shell_machine_channel.py -k 'windows or words' -n 0 -x --tb=short --basetemp "$R\checks\922-shell\pytest" --junitxml "$R\checks\922-shell\junit.xml"
# 18 passed, 126 deselected.
uv run pytest "$R\test_windows_previous_findings.py" -k 'not exact_base' -s -n 0 -x --tb=short --basetemp "$R\checks\922-fixes\pytest" --junitxml "$R\checks\922-fixes\junit.xml"
# 2 passed, 1 deselected: original quoting controls and traversal probe.
uv run pytest "$R\test_windows_new_findings.py" -k collision -s -n 0 -x --tb=short --basetemp "$R\checks\922-collisions\pytest" --junitxml "$R\checks\922-collisions\junit.xml"
# 1 failed: expected two preserved artifacts, got one with overwritten bytes.
uv run pytest "$R\test_windows_new_findings.py" -k quote -s -n 0 -x --tb=short --basetemp "$R\checks\922-quotes\pytest" --junitxml "$R\checks\922-quotes\junit.xml"
# 1 failed: both variants reached the intercepted executor.

Controls, with imports switched and checked against the named isolated snapshot:

# Predecessor 3b790620c54a4828b1067cc957e961668e2591c7:
uv run pytest "$R\test_windows_new_findings.py" -k collision -s -n 0 -x --tb=short --basetemp "$R\checks\922-prior-collision\pytest" --junitxml "$R\checks\922-prior-collision\junit.xml"
# 1 passed: two distinct deliveries with correct bytes.
# API base 82c76a82a9a7d312ef6cd75e560404bccd81d292:
uv run pytest "$R\test_base_quoted_host.py" -s -n 0 -x --tb=short --basetemp "$R\checks\922-base-quotes\pytest" --junitxml "$R\checks\922-base-quotes\junit.xml"
# 1 passed: both SSH variants refused before execution.

The two new probes intentionally assert the required behavior and fail at this head. All writes stayed inside the probe workspace; no SSH connection or remote transfer occurred. The shared node-client test race is tracked on #920 and its blobs are unchanged here. Live SSH/daemon/install operation and the full suite were not exercised.

@gloryfromca

Copy link
Copy Markdown
Member

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

I independently reproduced both boundary cases at a010ad8d2e2e: _local_name maps a\\b.txt and a%5Cb.txt to the same local name, while Windows tokenization splits adjacent quoted/unquoted host or port segments and therefore derives no destination or the wrong destination.

These are valid follow-ups, so my previous clean stance becomes nonblocking. At this late review round neither reaches the blocker bar: the collision requires two specially paired POSIX filenames on a cross-platform delivery, and the command bypass requires deliberately segmenting a single host or port across quote boundaries; ordinary filenames and the normal bare or whole-word-quoted command forms work. The focused repository suite still passes: 264 passed.

Handsome-wzw and others added 2 commits October 13, 2026 03:52
…of a Windows word stays in it

A fetched name's backslash was written as its percent-code and a
percent sign was not, so `a\b.txt` and `a%5Cb.txt` landed as one file
(reviewed 2026-10-12): the percent sign is written out too, and no two
names land as one. On a Windows host the words of a command were read
with the quotes kept and taken off afterwards, so a quoted piece next
to a bare one -- `"root@"host`, `"587"17` -- became two words and the
host or port went unmatched: the words are read with POSIX quoting,
pieces joined as that shell's argv has them, and only the backslash
kept as a path's own.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…equests, not as its last

The test of a link that comes back read the new link's last request as
the attach, and its heartbeat, every 10 ms there, can follow the attach
before the check: it now looks for the attach among the requests
(reproduced on Windows, 2026-10-12).

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
@Handsome-wzw

Copy link
Copy Markdown
Contributor Author

Both are on the branch now (598cc8d). A percent sign in a fetched name is written out as %25 too, so a\b.txt and a%5Cb.txt land as two files (test). On a Windows host the words are read with POSIX quoting and only the backslash kept as a path's own, so "root@"host and "587"17 are one word each, as that shell's argv has them (_words checked against those forms, and the ExecTool refusing both commands with the host stood in as Windows). The reconnect test race is fixed in 418923f on #920 and carried here.

@gloryfromca

Copy link
Copy Markdown
Member

No blockers; this can merge as far as I am concerned.

Confirmed at 2e9f7dd1105d: percent signs are now encoded before local landing, so the two reported remote names remain distinct; adjacent quoted and bare Windows command segments now form the host and port that the registered-machine guard checks; and the reconnect assertion remains scoped to the new client while tolerating a following heartbeat. The focused suite passes.

Handsome-wzw and others added 2 commits October 13, 2026 03:59
A review date in a source comment is the task's context, not the
code's: the five comments that carried one keep their reason and drop
the day.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
A review date in a source comment is the task's context, not the
code's: the four comments that carried one keep their reason and drop
the day.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>

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

Reviewed the new 10-file delta at 43f2eca16730 and reconciled it with the previously reviewed PR scope. The revised contract now matches the implementation: remote.shown() delegates to RemotePath.display(), which uses the registry user, host and port while excluding the key, and the ACP and CLI completion notices both use that path renderer. The corrections to the built-in-agent guidance also match the tools this revision actually exposes.

Coverage included AGENTS.md and the relevant CONTEXT.md terms, the revision diff and full PR diff inventory, affected callers and history, backward compatibility, test-strength changes, and the existing architecture constraints. No runtime behavior changed and no test was weakened. uv run pytest tests/test_acp_remote.py tests/test_subagent_acp_remote.py tests/test_subagent_cli_remote.py -q passed with 168 passed and 4 environment skips. git diff --check github/main...HEAD passed, and git merge-tree --write-tree github/main HEAD was clean against efe0f46aed75.

The previously recorded nonblocking follow-ups are unchanged.

Handsome-wzw and others added 2 commits October 14, 2026 11:04
… as the budget's own figure

A file just over deliver_files' budget rounds to the budget's own figure,
so the refusal read "20.0 MB is over the 20.0 MB limit" and said nothing
(a 20 MiB + 1 byte file on a registered machine; 20 MiB itself is fetched
whole). When the two round alike, the sentence gives the bytes of each.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…o no agent outlives a test

The fixture closed the machine's links before the ACP pool, and a link
closed on purpose leaves its agents with the daemon -- right for a host,
whose stop closes its pool first, and a leak for a test: the agent stayed,
the daemon never went idle, and every dispatch test of this module left
one of each behind (ten per run, counted on one computer). The pool
closes first now, the teardown checks that nothing is left under the
machine's daemons and ends a daemon that is, and one test shows the order
that the teardown relies on.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
@Handsome-wzw

Copy link
Copy Markdown
Contributor Author

A round of timing probes on this head, on a registered Linux machine, each run from one script against the real node (heartbeat 120 s, so the probe decides when a dead link is noticed). Ten probes; one wording defect found, fixed in 2542f2d, and the rest passed as they are.

  • Two conversations, one file: A read v1, the file became v2, B read it; A's whole-file write was refused ("changed after it was read; nothing was written", the file still v2); C, who never read it, wrote; B's write was then refused against C's; A read again and wrote. The digest is the reader's own, as the change says.
  • write_file with the link already dead (relay killed, unnoticed): answered in 0.42 s through a new link, the file written. With the relay killed 5 ms into a 1.2 MB write: "Error: the Raven node on machine 'RavenX 2' (ravenx2) stopped" in 0.01 s, the file as it was (4 bytes, never partial).
  • A link inside the root pointing outside it: a file link to /etc/hostname, a directory link to /etc, and a file under that link were each refused ("is outside allowed", the path shown as scp writes it); grep and find over the root found nothing through the link; the hostname never leaked.
  • deliver_files with the relay killed between chunk 1 and 2 of a 5 MiB file: delivered whole in 1.75 s, the next chunk having made a new link. Killed 3 ms into chunk 2: "Could not deliver: ... (the Raven node on machine 'RavenX 2' (ravenx2) stopped)" in 0.22 s, no .raven-part left, nothing partial landed.
  • The 20 MiB budget: exactly 20 MiB delivered whole in 3.7 s; 20 MiB + 1 byte refused, and the refusal read "20.0 MB is over the 20.0 MB limit". Fixed in 2542f2d: when the two figures round alike the sentence gives the bytes ("20,971,521 bytes is over the 20,971,520-byte (20.0 MB) limit for a file brought back from machine ..."), with a test at the edge that failed before it.
  • One file read and written on the daemon at once: page reads during a 1.35 MB write, page and digest agreeing every time (the daemon's file gate).
  • The lane switched off in the middle of the session: a read answers "File not found: ravenx2:raven-work/...", the path read as a local one as on main; a write lands under the workspace as a file of that spelling and nothing reaches the machine; deliver_files refuses; remote.machine refuses in one sentence; a link already open still answers a ping. Observed, as designed: with the lane off a path on a machine is a local path.

The script ends by cancelling whatever still runs on the machine through the protocol and listing what runs there: empty after each run. The test group passes with the new case, and the diff coverage of the fix is 100%.

Also carried here, as 8ba32ef: #921's test fixture fix from the same round (the stand-in machine closes the ACP pool before its links at teardown, so no agent or daemon outlives a test; b17c305 there). The remote test groups pass on this head (135) and leave nothing behind.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: make daemon discovery read complete process command lines and rebase this head onto current main.

The production change is sound: the strict byte budget is unchanged, the one-byte-over case now reports exact byte counts, and the focused delivery assertions cover both sides of the boundary. The new lifecycle test is not sound at this head, however, and the refreshed target branch no longer merges cleanly.

uv run pytest tests/test_deliver_files_tool.py tests/test_subagent_acp_remote.py -q produced 77 passes and one failure in the newly added lifecycle test. Running that test alone failed identically. Under PDB the daemon and agent were live, but the default-width ps output ended before the directory marker; requesting a wide process listing exposed the complete daemon argv. This satisfies the late-round blocker bar: this commit introduces the failing detector, the ordinary focused/CI test path reaches it, and the suite has no working way through without correcting the detector.

After git fetch github main, git merge-tree --write-tree github/main HEAD also exits 1 with conflicts in raven/acp_client/client.py and raven/agent/subagent/backends/cli_agent.py; GitHub reports the PR as dirty too. The branch must be rebased and those conflicts resolved before it can merge.

Coverage included AGENTS.md and the relevant CONTEXT.md contract, the complete PR diff inventory and exact three-file revision delta, affected callers and history, backward compatibility, test-strength changes, lifecycle/concurrency cleanup, and the existing architecture constraints. Patch-risk assessment bound 43f2eca16730..8ba32ef81ae6 at SHA-256 151aae39523f8ddd9f6b9812fc1963f849638a15c03395820b07c4a65e9f41f9; recommendation: revise.

Comment thread tests/test_subagent_acp_remote.py Outdated
@LivXue

LivXue commented Oct 14, 2026

Copy link
Copy Markdown
Member

Windows compatibility re-review at 8ba32ef81ae6eb1dd9ac3154b0ca2c28294c2913, base ee969dbc41a4b00978b4430aae9590a5ebfba010.

Reconciled the complete 76-file base-to-head diff with the previous Windows review, inspected the subsequent contract/message/test changes and reachable callers, and refreshed all comments, reviews and threads. Inspected the exact-byte delivery message change. git diff --check passed.

  1. P2, reproduced at this head - remote CLI command grammar. cli_agent.py:608 uses the Windows host's argv builder for a POSIX destination. The real _exec_there method sends sh -c 'hostname; pwd; cat' as POSIX argv ["sh","-c","'hostname;","pwd;","cat'"], splitting the script and breaking remote execution. Parse/build for the destination's POSIX grammar. This used a recording endpoint, without SSH.

  2. P2, reproduced at this head - IPv6 tunnel destination. remote.py:401 turns http://[::1]:8000/v1 into 127.0.0.1:41234:::1:8000. Windows OpenSSH's offline -G -F nul parser rejects the captured real tunnel argv (exit 255); the bracketed control exits 0. Bracket the destination IPv6 host in -L so remote OpenAI endpoints on IPv6 loopback can connect.

  3. P2, code-reading carry-forward - POSIX copy fixture. test_shell_machine_channel.py:837 still uses true ...; touch .... This unchanged fixture previously failed its marker assertion on Windows; it was not rerun here. Use a portable stand-in or platform-specific setup. This is not evidence that production SCP is refused.

The CLI/tunnel findings remain additions relative to the PR base, unchanged by these follow-ups. The contract wording and delivery wording changes introduce no additional Windows defect found in the inspected scope.

Exact native Windows verification commands and results

Python 3.13.12; isolated detached head, fresh uv environment, verified source imports, separate home/temp/token paths. Setup: uv sync --frozen --python 3.13 --all-packages --extra dev. Tests ran with PYTHONUTF8=1, UV_NO_SYNC=1, and UV_PROJECT_ENVIRONMENT=$R/envs/922. Scratch probes are outside the checkout.

$R = 'C:/Users/livxu/AppData/Local/Temp/raven-windows-review/fcff8ede0d4e4d30a901faa90a675728'
$cases = @(
  'tests/test_ops_remote_path.py',
  'tests/test_acp_remote.py::test_a_path_on_the_machine_is_shown_the_way_scp_writes_it',
  'tests/test_acp_remote.py::test_an_address_a_model_reads_is_scp_shaped_by_address_and_never_by_id',
  'tests/test_acp_remote.py::test_the_files_note_names_what_came_with_the_task_by_address_and_nothing_else',
  'tests/test_subagent_cli_remote.py::test_the_roster_names_the_machine_for_every_kind_and_hands_it_no_paths',
  'tests/test_subagent_cli_remote.py::test_a_built_in_row_cannot_name_a_machine_and_is_told_so',
  'tests/test_rpc_subagents.py::test_a_built_in_agent_is_refused_a_machine_in_one_sentence',
  'tests/test_subagent_acp_remote.py::test_a_dispatch_runs_on_the_machine_in_a_directory_made_there_and_says_where',
  'tests/test_subagent_cli_remote.py::test_a_cli_dispatch_runs_on_the_machine_in_a_directory_made_there_and_says_where',
  'tests/test_subagent_acp_remote.py::test_the_pool_closed_before_the_links_leaves_no_agent_and_the_daemon_goes_idle'
)
uv run pytest @cases -n 0 -x --tb=short --basetemp "$R/checks/922-updated/pytest" --junitxml "$R/checks/922-updated/junit.xml"
# 98 passed, 3 skipped
uv run pytest "$R/test_windows_remote_cli.py" -s -n 0 -x --tb=short --basetemp "$R/checks/922-cli-current/pytest" --junitxml "$R/checks/922-cli-current/junit.xml"
# 1 failed: remote command argv differs as described above
uv run pytest "$R/test_windows_remote_ipv6.py" -s -n 0 -x --tb=short --basetemp "$R/checks/922-ipv6-current/pytest" --junitxml "$R/checks/922-ipv6-current/junit.xml"
# 1 failed: forwarding specification rejected; bracketed control passes
uv run pytest tests/test_deliver_files_tool.py -n 0 -x --tb=short --basetemp "$R/checks/922-deliver/pytest" --junitxml "$R/checks/922-deliver/junit.xml"
# 32 passed

Coverage limits: three POSIX-shell cases skipped, including the new daemon teardown test; its Linux failure is already discussed. No real SSH/remote installation, POSIX daemon end-to-end run, full repository suite or Windows ACL audit.

…files

# Conflicts:
#	raven/acp_client/client.py
#	raven/agent/subagent/backends/cli_agent.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.

Blocking: make daemon discovery request complete process command lines.

The merge with current main now resolves cleanly, and I found no additional defect in the two formerly conflicting ACP/sub-agent files. The standing daemon-discovery blocker is unchanged, however: the exact-head focused test still fails with box.daemons() returning no process before cleanup is exercised.

Validation:

  • uv run pytest -n0 tests/test_subagent_acp_remote.py::test_the_pool_closed_before_the_links_leaves_no_agent_and_the_daemon_goes_idle -q -s: failed (1 failed) at the same daemon-discovery unpack.
  • git diff --check github/main...HEAD: passed.
  • git merge-tree --write-tree github/main HEAD: passed against d106a5c6ef32.

I covered the current-base final comparison and merge resolution, the affected callers and history, AGENTS/CONTEXT constraints, backward compatibility, test assertions and weakening risk, and the remote-process architecture boundary. The immutable final diff is d106a5c6ef32...63eb101e379d (76 files, SHA-256 06c45c356e01fce46228dc5aaf2fcd66526a824515a84c4560950e43b6a89b46); the patch-risk assessment validates as revise.

…he terminal width

The lookup that finds the daemons serving the stand-in machine read the
process table with ps at the terminal's width, and on Linux the daemon's
command line runs past 80 columns before the directory that names the
machine, so a narrow run listed none of them: the lifecycle test failed
on its first line, and the fixture's leak check could neither see nor end
a daemon it should have. ps is asked for whole command lines now, and a
test plants a process with a long command line under an 80-column setting
and finds it. The fixture's comment keeps its reason and drops the count
of what an earlier run left behind.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>

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

Confirmed the daemon-discovery fix at 32e6ee9adb54. The new long-command regression and the previously failing cleanup case pass, the complete remote-ACP module passes (47 tests), and an independent narrow-versus-wide ps probe reproduces the control and the fix. git diff --check and merge-tree against current main also pass.

The existing nonblocking Windows follow-ups remain: destination-POSIX quoting for remote CLI commands from Windows, brackets around IPv6 tunnel hosts, and portability of the POSIX copy fixture. This one-file revision introduces no additional issue.

I reviewed the revision delta, its fixture caller and process-discovery behavior, the test assertions for weakening, current-base mergeability, relevant history, repository rules, compatibility implications, and the previously recorded architecture constraints. All threads I opened are settled.

@LivXue

LivXue commented Oct 14, 2026

Copy link
Copy Markdown
Member

Windows compatibility re-review at 32e6ee9adb547eb7b507a16e18c811722d1f6466, base d106a5c6ef329c6bc61f29be538818dff4afaa86.

Reconciled the complete 76-file base-to-head diff with the previous review: 59 files remain byte-identical; inspected the other 17 files against the new base and prior head, including upstream changes and the ACP endpoint/process-cleanup integration. Also inspected the remote CLI merge and POSIX daemon-discovery test update. All discussion pages and current threads were checked; git diff --check passed.

Native checks: 187 passed, 36 skipped; the two existing runtime defect probes still fail as described below. No additional Windows defect found in the merge resolution or daemon-fixture delta.

  1. P2, reproduced - remote CLI grammar. cli_agent.py:640 still uses the Windows host's argv builder for the POSIX destination. The real _exec_there sends sh -c 'hostname; pwd; cat' as POSIX argv ["sh","-c","'hostname;","pwd;","cat'"], splitting the script. Parse/build for the destination grammar. The recording-endpoint probe makes no SSH connection.

  2. P2, reproduced - IPv6 tunnel destination. remote.py:401 turns http://[::1]:8000/v1 into 127.0.0.1:41234:::1:8000. Windows OpenSSH's offline -G -F nul parser rejects the captured tunnel argv (255); the bracketed control succeeds (0). Bracket the IPv6 destination in -L so this endpoint can be reached.

  3. P2, code-reading carry-forward - POSIX copy fixture. test_shell_machine_channel.py:837 remains unchanged and uses true ...; touch ..., the previously reproduced Windows marker-assertion failure. Use a portable stand-in or platform-specific setup. Not rerun here; this does not prove production SCP is refused.

The CLI/tunnel paths are absent from the new base; these remain PR additions, not regressions introduced by this merge/follow-up.

Exact native Windows verification commands

Python 3.13.12; detached exact-head checkout, verified source imports, fresh uv environment and isolated home/temp/token paths. Setup succeeded: uv sync --frozen --python 3.13 --all-packages --extra dev. Tests used PYTHONUTF8=1, UV_NO_SYNC=1, and UV_PROJECT_ENVIRONMENT=$R/envs/922. Scratch probes stay outside the checkout.

$R = 'C:/Users/livxu/AppData/Local/Temp/raven-windows-review/2a3b9a778d94475783ca7bb0f0c52730'
$hostCases = @(
  'tests/test_ops_remote_path.py',
  'tests/test_acp_client_launch.py',
  'tests/test_acp_client_teardown.py',
  'tests/test_node_bundle.py',
  'tests/test_node_client.py',
  'tests/test_node_server.py',
  'tests/test_node_relay.py',
  'tests/test_filesystem_tools.py::test_a_page_is_the_lines_splitlines_gives_whatever_the_chunk_edge',
  'tests/test_filesystem_tools.py::test_a_line_longer_than_a_chunk_is_whole_on_its_page_and_only_counted_off_it',
  'tests/test_filesystem_tools.py::test_a_long_line_is_looked_at_once_whatever_its_length',
  'tests/test_filesystem_tools.py::test_read_file_pages_a_file_without_holding_all_of_it',
  'tests/test_subagent_acp.py::test_close_without_killpg_ends_the_tree_through_taskkill',
  'tests/test_subagent_acp.py::test_close_without_killpg_kills_nothing_by_the_pid_of_an_exited_launcher',
  'tests/test_subagent_acp_remote.py::test_the_pool_closed_before_the_links_leaves_no_agent_and_the_daemon_goes_idle',
  'tests/test_subagent_acp_remote.py::test_the_daemon_lookup_reads_whole_command_lines_whatever_the_terminal_width'
)
uv run pytest @hostCases -n 0 -x --tb=short --basetemp "$R/checks/922-host/pytest" --junitxml "$R/checks/922-host/junit.xml"
# 151 passed, 36 skipped
$killCases = @(
  'tests/test_subagent_third_party.py::test_cli_backend_cancel_without_killpg_ends_the_tree_through_taskkill',
  'tests/test_subagent_third_party.py::test_cli_backend_timeout_without_killpg_still_reports_the_timeout',
  'tests/test_subagent_third_party.py::test_cli_backend_timeout_without_killpg_ends_the_launcher_when_taskkill_does_not_answer',
  'tests/test_subagent_third_party.py::test_cli_backend_without_killpg_kills_nothing_by_the_pid_of_an_exited_launcher'
)
uv run pytest @killCases -n 0 -x --tb=short --basetemp "$R/checks/922-cli-kill/pytest" --junitxml "$R/checks/922-cli-kill/junit.xml"
# 4 passed: native cancellation/timeout and exited-launcher checks
uv run pytest "$R/test_windows_remote_cli.py" -s -n 0 -x --tb=short --basetemp "$R/checks/922-cli-quote/pytest" --junitxml "$R/checks/922-cli-quote/junit.xml"
# 1 failed: remote argv split
uv run pytest "$R/test_windows_remote_ipv6.py" -s -n 0 -x --tb=short --basetemp "$R/checks/922-ipv6/pytest" --junitxml "$R/checks/922-ipv6/junit.xml"
# 1 failed: forwarding specification rejected; bracketed control passes
uv run pytest tests/test_deliver_files_tool.py -n 0 -x --tb=short --basetemp "$R/checks/922-deliver/pytest" --junitxml "$R/checks/922-deliver/junit.xml"
# 32 passed

Limits: no real SSH/remote installation, POSIX daemon end-to-end execution, full repository suite or Windows ACL audit. The new ps -ww regression and daemon teardown case skip at the POSIX-shell guard on this host; their Linux success was not reproduced here. Git HTTPS transport failed; GitHub's Git data API supplied missing objects, each SHA verified before checkout. This did not reduce the source diff.

Handsome-wzw and others added 2 commits October 14, 2026 12:24
…ine, IPv6 endpoints bracketed

Three findings from the agent and Windows reviews of this PR, each with a
test that failed on the code before and a run on a registered machine with
the code before and after.

- An openai entry on a machine told its endpoint about the files that came
  with the task with this computer's note (absolute paths under
  "[attachments, by absolute path]"), as if the endpoint could read them.
  It now says what the acp and cli lanes say: the files by their address
  on the computer that sent them, nothing copied, say so and go on where
  one cannot be found (remote.files_note). The machine is read from the
  registry once per turn, before the message is built, and the tunnel is
  opened with it.
- A cli entry on a machine had its command split by this computer's
  grammar (command_argv: CommandLineToArgvW on Windows), so from a Windows
  host `sh -c 'hostname; pwd; cat'` reached the machine as five words and
  its shell refused the script. _build_argv takes the splitter, and the
  launch on a machine passes shlex.split: the node runs the command under
  a POSIX shell, whatever this computer is.
- A tunnel to an endpoint on the machine's IPv6 loopback was asked of ssh
  as `-L 127.0.0.1:<port>:::1:8000`, which ssh refuses; the address is
  bracketed now, as it is in the URL.

The stand-in forwarder reads a bracketed far host, and the comment on the
address shown in a reply no longer says the file tools read it, which this
PR's do not.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…s host too

`true` and `touch` stood for the copy command and its marker, and neither
is on a Windows host, where the test failed on the marker. Python touches
the marker now, with the copy command's words as its arguments, as before
them: what the test checks is unchanged -- a line with scp, sftp or rsync
to a registered machine in it is the shell's to run, and only a raw ssh
to one is refused.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
@Handsome-wzw

Copy link
Copy Markdown
Contributor Author

Carries 4ebe198 from #921 (an openai row's files named by address, a cli command split for the machine's shell from a Windows host, an IPv6 endpoint's forward bracketed; the before and after runs are on that PR). The copy test's fixture is portable in a093a86: python touches the marker in place of true and touch, with the copy command's words as its arguments, so the four cases run on a Windows host as they do here.

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

Confirmed at a093a86641c5: remote CLI commands are split with the destination node's POSIX grammar even on a Windows host; IPv6 endpoint literals remain bracketed in the SSH -L specification and work through the stand-in tunnel; the copy fixture uses the current Python executable instead of POSIX-only utilities; and OpenAI rows on a machine describe task files by address without claiming they were copied.

Validation: the two complete affected suites pass (156 tests), including all four copy spellings, the Windows-host splitter stand-in, IPv6 formatting and loopback forwarding, and the remote OpenAI file-note contract. git diff --check and merge-tree against current main pass.

I reviewed the seven-file revision delta, affected callers and callees, prior findings and history, repository rules and domain contracts, local/backward compatibility, assertion strength, Windows/POSIX behavior, and the remote command/tunnel/file-address architecture boundaries. The prior nonblocking follow-ups are resolved, no new finding remains, and all threads I opened are settled.

@0xKT

0xKT commented Oct 14, 2026

Copy link
Copy Markdown
Member

Merge conflict: @Handsome-wzw #920 and #940 have merged into main, advancing the tip to ea9bb50, and this PR no longer merges onto it cleanly. Re-running the merge check against the live tip:

git merge-tree --write-tree ea9bb502 a093a866

reports add/add conflicts in raven/node/client.py, raven/node/protocol.py, raven/node/server.py and raven/ops/remote_path.py (this PR carries its own copy of the node subsystem and remote_path that #920 just landed), plus content conflicts in raven/acp_client/remote.py, CONTEXT.md, tests/test_acp_remote.py, tests/test_node_client.py, tests/test_node_server.py and tests/test_ops_remote_path.py. The judge has cleared the current head, so a rebase onto the latest main, resolving these against the now-merged #920 code, is the one step left before this can enter the acceptance lane. Posted by the review board after re-running the merge check.

@gloryfromca

Copy link
Copy Markdown
Member

Blocking: rebase onto current main and resolve the integration conflicts.

Independently confirmed against live main at ea9bb502cba1: git merge-tree --write-tree github/main HEAD exits 1. The conflicts include CONTEXT.md, raven/acp_client/remote.py, add/add collisions in the node and remote-path implementations, and their corresponding tests. My clean stance on a093a86641c5 is therefore superseded until the branch is reconciled with the code that landed from #920/#940.

Merge main after the node service landed. The ten files main carries from
that service were byte-identical to the reviewed node head, so this
branch's own read, write and deliver changes are kept whole on top of it,
and the follow-ups main took afterwards are retained beside them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@LivXue

LivXue commented Oct 14, 2026

Copy link
Copy Markdown
Member

No blockers in the reviewed scope.

This supersedes my summary at 2ff2c78, which opened Blocking:. Both blockers filed in that round are fixed at this head, and so is the Blocker I filed on #921 at raven/agent/subagent/backends/__init__.py:103, which this PR carries. This round files no new finding.

Scope: I reviewed only the raven/agent/** portion of this PR -- 14 of 76 changed files, seven under raven/agent/tools/ and seven under raven/agent/subagent/. The seven under raven/agent/subagent/ have the same content as #921 at 4ebe198. The remaining 62 files were read as context but are not covered by this verdict.

What I examined: the changes since 2ff2c78 -- the copy refusal removed from shell.py and machine_exec.py, the read digest keyed by its reader in remote_files.py, deliver.py, the paged read_file in filesystem.py, the raven_config.py hints, and the machine lane widened to every kind under raven/agent/subagent/ with its later fixes (the written address rule, a command for a machine split by shlex.split, an openai row's files named by address) -- against the permission defaults and session keys under raven/permissions/ and raven/ops/remote_path.py.

Confirmed findings: none in this round.

My previous round's findings, at this head:

  • The scp, sftp and rsync refusal is gone (shell.py, Blocker). With recording stand-ins for the clients on PATH, the four copy commands I used last round ran with the machine lane on and with it off; at 2ff2c78 the same four were refused. A plain ssh to the machine is still refused, and the refusal again says scp and rsync to it are fine.
  • A write on a registered machine now asks (raven/permissions/rules.py, Blocker). default_tier returned ask for write_file and edit_file on all four remote paths that note measured, mode="append" included, and a write grant is keyed by the one file it names. Reads are unchanged.
  • The digest a write expects is keyed by its reader (remote_files.py). Through the stand-in ssh and a real node, with three conversations, another agent's read no longer lets a stale overwrite through.
  • Nothing reclaims pulled/, now on purpose, as the _landing docstring says (deliver.py).
  • The design ids C28 and C30 are gone from raven/, tests/, CONTEXT.md, docs/ and docs-site/.

These were measured at 6970b45. The files they rest on are unchanged since, except deliver.py, whose only change is the wording of its size refusal, and the C28/C30 search is empty at this head.

The notes I filed on #921, at this head:

  • The address rule (backends/__init__.py:103, Blocker): the five sites it named -- the AgentMeta.machine docstring, _WAY_IN and Machine in raven/acp_client/remote.py, and the two sentences of the CONTEXT.md entry -- now state the rule with its format. remote.shown gives scp://worker@203.0.113.7:58717//home/worker/raven-work/t1 off port 22 and worker@203.0.113.7:/home/worker/raven-work/t1 on it: the registry's address, no machine id, no key. test_an_address_a_model_reads_is_scp_shaped_by_address_and_never_by_id passes, and fails when shown names the path by the machine's id.
  • An openai row's attachments (openai_api.py:287, Nonblocking): a row on a machine now names the files that came with its task by address, as the acp and cli lanes do. test_the_files_that_came_with_an_openai_row_s_task_are_named_by_address_and_not_copied passes, and fails with the local note restored.

The three findings of the Windows review on this PR (cli_agent.py, remote.py and test_shell_machine_channel.py, last reported at 32e6ee9) are addressed by a2cee17 and a093a86: a command for a machine is split with shlex.split whatever this computer is, an IPv6 tunnel host is bracketed, and the copy fixture no longer runs true and touch. The splitter test passes, and fails with this computer's splitter restored. I checked these on Linux only; the native Windows run has not been repeated at this head.

Verification:

  • At this head, six test files over what the commits since 6970b45 change (test_subagent_cli_remote.py, test_subagent_acp_remote.py, test_acp_remote.py, test_deliver_files_tool.py, test_shell_machine_channel.py, test_node_client.py), run in an environment whose imports resolve to this head: 386 passed, 7 skipped (no csh, tcsh or zsh on this machine, and an OpenSSH older than 9.2). At 6970b45, 13 test files across the same areas: 782 passed, 1 skipped.
  • lint-imports at this head: 10 kept, 0 broken.
  • The reproductions were run on Linux, through the PR's stand-in ssh and its real Raven node where a node is involved. No connection to a real machine was made.

Checked and deliberately not reported:

  • test_a_cancel_ends_the_agent_s_whole_process_tree_on_the_machine failed once in CI at 6970b45 and passes at this head, in CI and in the run above. Under load it failed 2 of 18 runs at 0fa61f1 and 0 of 18 at 6970b45, so I did not treat it as a defect of this change; I did not establish whether a cancel came late or was lost.

The branch now conflicts with main: #920 landed its own copy of the node and remote-path subsystem this PR also adds, and the tip was checked at ea9bb50. git merge-tree --write-tree ea9bb502cba1 a093a86641c5 exits 1, with add/add conflicts in raven/node/client.py, raven/node/protocol.py, raven/node/server.py and raven/ops/remote_path.py, and content conflicts in raven/acp_client/remote.py, CONTEXT.md and their tests -- the same list 0xKT and gloryfromca reported. Nothing under raven/agent/ is in that list, so it does not change the verdict above, which is scoped to those 14 files. It does mean the file tools will read through #920's copy of raven/ops/remote_path.py once this branch is rebased onto main; that file is not this PR's, and the checks above were run against this PR's copy.

@Handsome-wzw

Copy link
Copy Markdown
Contributor Author

Rebased the integration: origin/main (ea9bb50) is merged into this branch as de6e70e, plain push, no rebase and no force.

How it was resolved: the ten files main carries from the node service on this head are byte-identical to the node branch's final head, so this branch's own read, write and deliver changes were kept whole on top of them, and the follow-ups main took afterwards are retained beside them. Checked file by file, and the merge result against both sides matches exactly what each side alone had.

Validation on the merged tree, and the ten real-machine probes will follow in a comment.

@Handsome-wzw

Copy link
Copy Markdown
Contributor Author

A round of timing probes on this head, on a registered Linux machine, each run from one script against the real node (heartbeat 120 s, so the probe decides when a dead link is noticed). Ten probes, 10 PASS:

  • Two conversations, one file: a read's digest is its reader's own, and the whole-file write of the one who read an older version is refused with nothing written.
  • write_file with the link already dead (relay killed, unnoticed): answered in 0.31 s through a new link, the file written. Killed 5 ms into a 1.2 MB write: refused in 0.01 s, the file as it was, never partial.
  • A link inside the root pointing outside it: a file link, a directory link and a file under that link each refused, and grep and find over the root find nothing through them.
  • deliver_files with the relay killed between chunks of a 5 MiB file: delivered whole in 1.34 s on a new link. Killed 3 ms into the second chunk: refused in 0.13 s, no .raven-part left, nothing partial landed.
  • The budget at the edge: exactly 20 MiB delivered whole in 2.8 s; 20 MiB + 1 byte refused, and the sentence gives both figures in bytes.
  • One file read and written on the daemon at once: page and digest agreed every time.
  • The lane switched off mid-session: a read of an address answers as a local path, a write lands inside the workspace and nothing reaches the machine, deliver_files refuses, machine() refuses in one sentence, and a link already open still answers a ping.

The script cancels whatever still runs on the machine through the protocol and lists what is left: empty. The node installed for the run was removed afterwards.

Merge commit de6e70e; node digest a058d39a402a.

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

Confirmed at de6e70eb6c07: the branch merges cleanly with current main (ea9bb502cba1). The ten formerly conflicting node, remote-path, context, and test files preserve the already-reviewed branch blobs; nine of the thirteen effective merge additions are byte-identical to main. The remaining probe.py and associated test integrations preserve both sides and add current-main's newest-measurement-wins guard without disturbing the registered-machine paths.

Validation: 981 exact-head focused tests passed with four expected unavailable-shell skips across node client/server, remote paths, ACP transport, remote file tools and delivery, capability probing, Windows environment handling, remote CLI, and shell-machine behavior. git diff --check and current-main merge-tree pass.

I covered the current-base final comparison, conflict resolutions against both parents, callers and history, repository and domain rules, backward compatibility, test weakening, remote authorization/filesystem boundaries, destination command and tunnel parsing, node recovery/concurrency, and the capability-snapshot merge. The immutable 50-file comparison is ea9bb502cba1...de6e70eb6c07 (SHA-256 9ab11e18e8e79c3acacf455084490146af8f49ea9fb2b4b3301b21111ff4e77f); the validated patch-risk recommendation is merge / human_review_required because the remote-runtime impact is high and native Windows was not rerun after this merge. No source-visible defect remains, the integration blocker is cleared, and all threads I opened are settled.

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

Approving at de6e70e. The scope and the evidence are in my summary comment above.

@Handsome-wzw
Handsome-wzw merged commit b43d78b into EverMind-AI:main Oct 14, 2026
25 of 30 checks passed
Handsome-wzw added a commit to Handsome-wzw/Raven that referenced this pull request Oct 14, 2026
Merge main with EverMind-AI#922 squashed in (b43d78b). The five files that
conflicted -- the acp and cli backends, the node client and the two
stand-in-machine test files -- already carry main's side byte for byte,
since EverMind-AI#922's commits were brought in before; the conflicts are this
branch's own changes beside them, kept whole: the run's record and
pick-up on the acp and cli lanes, the frame count a channel hands over,
the transcript parse moved into a helper the pick-up shares, and the
tests of a run left and picked up. Nothing of main is dropped: what
these files lose against main are lines this branch rewrote on purpose.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
0xKT pushed a commit that referenced this pull request Oct 14, 2026
…er stops (#934)

## Summary

Registered machines are now available without a debug environment
switch.
`RAVEN_DEBUG_MACHINES` previously defaulted to off: it kept
machine-backed
agents out of the dispatch table, refused node/installer access, hid
remote
preset destinations, and treated file paths locally while off. It was a
trial feature gate, not a verbose-logging option. At the owner's
request,
this PR removes the variable and all of those checks. Normal startup and
restart recovery use the configured machine entries; even a leftover
value
of `0` is ignored. Machine registration, SSH authentication, file
permission
prompts, and concurrency limits still apply.

Fourth change in the series that lets a Raven dispatch work to the
machines in its machine registry. #920, #921 (merged through #922) and
#922 are on main; this change stands on main alone now, and the diff is
its own.

What this does: a turn running on a registered machine survives this
computer stopping, and the next start picks it up and reports it.

- While a turn runs on a machine, this computer remembers what the
  machine does not know (`raven/acp_client/remote_runs.py`): one
  owner-only file per run under `<raven home>/machines/runs/`, with the
machine's registry id, the task id its daemon runs the agent under, the
  agent, the session key, the instance handle, the dispatch id, the ACP
  session, the id of the prompt whose answer is awaited, the ids this
  side has used, how many of the agent's frames were read here, the
  agent's answer to `initialize` and the connection's launch key. Kept
  current after every frame the connection has dealt with, removed when
  the turn ends here. No address is in it.
- When this process stops -- the gateway, `serve`, the TUI, the page
  stack -- the
  agents on machines stay with their daemons
  (`remote.leave_agents_running()` before the drain): a cancelled turn
on a machine sends no `session/cancel`, closing its connection ends the
endpoint only, and the manager leaves the dispatch's record open rather
  than finishing it as cancelled. A hard kill or `/restart` leaves the
  same record, written as the turn ran.
- At the next start -- the gateway once its generation is bound; the
`serve`, TUI and page stacks once --
`raven/agent/subagent/resume_remote.py`
picks every run up: `task/list` on the machine, `agent/attach` from the
  frame last read (`NodePool.attach`), the connection adopted into the
  pool with request ids carried on past the other process's
  (`AcpConnectionPool.adopt`, `AcpClient.continue_ids_from`), the
  prompt's answer awaited (`AcpClient.await_response`), the reply built
  as a turn ended here is -- the partial notice when the agent stopped
  early, the machine note saying it was picked up after this computer
  restarted -- then the dispatch's record finished, the registry row and
  the node claim closed, and the result announced into the conversation
  that asked, as the manager announces a turn that ended here
  (`raven/acp_client/remote_resume.py`). A machine that cannot be asked
  keeps its record for a later start; a run the machine no longer has,
  one whose agent ended while this computer was away, one whose agent
  left the agent table, and one whose turn says nothing are announced as
  errors that say so. The adopted agent process serves later dispatches
  of its entry, as one started here would.
- Seams: `AcpClient.request(sent=)` hears the id of what goes out;
  `ChannelEndpoint.watchers` hear the frames the connection has dealt
  with (`consumed`, called by the read loop); `ChannelEndpoint.stays`
  reads the leaving flag.
- A cli entry's run on a machine is recorded while it runs (kind `cli`,
  no ACP part) and left the same way, the manager reading the dispatch
  as left; the entry picks it up (`CliAgentBackend.pick_up`): the daemon
  replays the command's whole output from its first line, the exit code
  ends it, and the reply closes with the machine note.
- The record carries the task as the model wrote it, and -- once the
  manager has read the run as left -- the dispatch's origin and summary
  (`remote_runs.annotate`): the announce after a restart names the
  instance, node and tool call as the announce here does and quotes the
  task as authored, never the rendered prompt on disk; a host killed
  outright gets an origin made up from the spawn record and the run.
- The record goes last, once the outcome has reached its conversation:
  a conclusion that fails, a stop under it, or an announce the scheduler
  refuses (`_announce_result` says so now) leaves the record, its claim
  released, for the next start to try again.
- A run that returned here keeps its record the same way, until the
  manager's announce of the reply was taken: the manager dispatches with
  `reported_by_caller=True`, the backend then leaves the record behind
  with the reply instead of removing it as it returns, and the manager
  removes it after the announce (`remote_runs.forget_dispatch`) -- or,
  when the draining scheduler refused the announce, annotates it with
  the dispatch's origin and summary for the next start to report. A host
  killed between the agent's answer and the announce therefore still has
  the way back to the run. A caller that takes the reply as its own
  result (a graph node, a probe, a direct chat) dispatches without the
  keyword, and the record goes as it did. The record's frame count stops
  at the prompt: the turn's own frames, the chunks before the answer as
  much as the answer, are never checkpointed, so a pick-up replays the
  turn whole and the reply it reports is complete; a record that had
  moved past the answer would have resumed after it and waited forever.

Not in this change: a run from the DAG lane has no spawn record to
finish; it is announced from the origin the manager added to its record
when it was left, or, for a host killed outright, from the run's record
alone. The Tasks page reads the finished record on its next load rather
than live.

## Type

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

## Verification

Machine availability and restart checks after removing the trial switch:

- New default-behavior regressions were run before the removal: 8
failed,
  with the enabled-value control passing. After removal, all 9 passed,
  including ACP/CLI recovery through the real AgentRegistry with the
  variable absent or set to `0`.
- `uv run --no-sync pytest tests/test_acp_remote.py
tests/test_node_client.py tests/test_node_server.py
tests/test_node_install.py tests/test_ops_remote_path.py
tests/test_permissions_remote_read.py tests/test_remote_file_tools.py
tests/test_deliver_files_tool.py tests/test_raven_config_tool.py
tests/test_rpc_subagents.py tests/test_shell_machine_channel.py
tests/test_subagent_acp_remote.py tests/test_subagent_cli_remote.py
tests/test_resume_remote.py tests/test_remote_resume.py
tests/test_remote_runs.py tests/test_subagent_manager.py
tests/test_cli_tui_commands.py tests/test_rpc_schema_match.py
tests/test_config_schema.py -n 4 --idle-ceiling-strict --cov=raven
--cov-branch --cov-report=json:<coverage-json> --cov-report= --tb=short`
  -- 1799 passed on the combined tree, including the latest published
result-retention fix. An earlier run had a missing stdio response in the
minimal-node dependency test and an idle-gate hit; both passed a focused
recheck, and this final complete selection passed with the strict gate.
- `uv run --no-sync python scripts/coverage_gate.py --coverage-json
<coverage-json> diff --base-ref origin/main --threshold 90`
-- 92.92% (656/706 changed executable lines) for this PR; the
gate-removal
commit alone covers 2/2 new executable lines (the remainder deletes
gates
  and updates descriptions).
- `UV_NO_SYNC=1 make lint-python lint-imports lint-deps lint-types`, the
commit/source-language/large-file scope gates and `git diff --check`
pass.
Both RPC generators' `--check` runs pass; `npm test --
scripts/gates/fixture-shape.test.mjs`
  from `ui-web` passes all five tests.
- Real Linux machine, isolated node installations, separate dispatch and
restart processes: before removal, a task started with the trial switch
on was reported as an absent agent and its recovery record deleted when
  restarted without it. After removal, both absent and old `0` settings
  recover the full CLI result and read its output file by address. The
absent-variable case kills the relay before host shutdown. The combined
  final tree was reinstalled and both scenarios repeated successfully.
- With the variable absent, 12 further timing/boundary probes pass:
early
  CLI exit and blank lines, relay loss before heartbeat with normal-call
recovery, tunnel loss, ACP/CLI/OpenAI cancellation during disconnection,
  11 launches admitting 8 and refusing 3, occupied tunnel port, and an
unwritable prompt file. Three attachment, POSIX-command and IPv6 checks
  also pass; Windows command parsing is simulated, not native Windows.
No test task or tunnel remains; the dedicated node directories are
removed.
- CONTEXT.md, configuration and RPC descriptions, transport/path
docstrings,
  both collaboration guides and the owner's design notes were checked
against the code. The guides explain the former trial switch and normal
startup without it. The existing SSH, permission and limit tests
continue
  to exercise their checks with no test-only switch setup.

- `env -u RAVEN_DEBUG_MACHINES uv run --no-sync pytest -n 4 --tb=short
--cov=raven --cov-branch --cov-report=json:<full-coverage-json>
--cov-report=`
-- 31345 passed, 230 skipped, 2 xfailed, 5 failed. A separate
current-main
  checkout reproduces four failures: CLI color rendering on this host,
LibreOffice detection, and the two simulation cases. The remaining
failure
  was the direct-chat test stub rejecting `reported_by_caller` from the
latest result-retention change; the published wrapper fix now forwards
only keywords declared by its fake backend. The full
suite was not rerun for that test-only correction. Final direct-chat and
manager recheck: 277 passed with strict idle accounting. Full-PR
changed-line coverage from this
  run is 96.18% (679/706).

Earlier verification:

- `uv run pytest tests/test_remote_runs.py tests/test_remote_resume.py
tests/test_resume_remote.py tests/test_acp_client_resume.py
tests/test_node_client.py tests/test_subagent_acp_remote.py
tests/test_subagent_cli_remote.py tests/test_cli_tui_commands.py
tests/test_subagent_manager.py tests/test_rpc_bootstrap.py
tests/test_cli_gateway_commands.py tests/test_acp_client_launch.py
tests/test_subagent_acp.py tests/test_scope_canon.py
tests/test_l4_entrances.py`
-- all passed (605 passed). Covered: the record written owner-only, kept
current
per frame and per prompt, removed when the turn ends here and kept when
  this process leaves; a dispatch left at shutdown neither finished nor
  announced by the manager; an endpoint ended while leaving leaves the
  agent with the daemon (real daemon); a task picked up from the frame
  last read, with the watchers hearing each frame; a pick-up the machine
  refuses or cannot carry; an answer awaited for a request another
  process sent, ids carried on, no `session/cancel` while leaving; the
  reply read off replayed frames with the other sessions' frames
  ignored, a turn cut short marked partial, an unreachable machine
  keeping the record, a run the machine no longer has, a prompt never
  sent, an agent that ended while this computer was away, an answer
  waited out, a turn that said nothing, a pick-up beside a live
  connection closing its own; the manager-side conclusion: record
  finished and announced, an error announced and the record failed, an
  unreachable machine keeping the record, an agent off the table, a
  failure inside one pick-up not stopping the next, a run with no
  conversation, the once-per-process scheduling; a cli run recorded
  while it runs and left at shutdown, picked up whole on the real daemon
  behind the stand-in ssh, its pick-up's errors said (a non-zero exit, a
  machine that no longer has it, a link down, this computer leaving), a
  cancel that is not a leave ending the command with no record left; the
  TUI's stop saying the agents stay before the cancel and its start
  scheduling the pick-up; the record annotated with the dispatch's
  origin and summary when the manager reads it as left, the announce
  quoting the task as authored and naming instance, node and tool call,
  with and without the manager's fields; the record outliving a refused
  announce and a failed conclusion, claim released; a cli record read
  without an ACP session, an older record read with defaults; the record
  moving with the frames before the prompt and not past it, a dispatch's
  record forgotten by the manager's id with its claim, the manager
  dispatching with `reported_by_caller` and forgetting the record after
  an announce that was taken and annotating it after one that was
  refused; on the real daemon behind the stand-in ssh, an acp reply and
  a cli output returned with the record kept and the host "stopped"
  before the announce, the next start reading the same reply again
whole, and a dispatch without the keyword leaving no record. End to end
on the real
  daemon behind the stand-in ssh: a turn dispatched, this process
  "stopped" the way the gateway stops, the agent still running on the
  daemon, then a fresh pick-up reading its answer, finishing the record
  and announcing it, with the agent's process the pool's connection
  again.
- `uv run pytest` on this tree, at an earlier revision of this change --
29270 passed, 121 skipped, 3 failed:
  `test_rpc_files.py::test_a_host_without_libreoffice_says_so`,

`test_simulation_scenario.py::test_a_fresh_trial_refreshes_the_subagent_homes_before_it_runs`
and
`test_simulation_suite.py::test_a_stopped_run_is_marked_in_its_record_and_finished_with_its_evidence`,
  which fail the same way on main on this machine; diff coverage of this
  change against its base: 97.45% (153 of 157 executable changed lines),
  and 94.67% for the four changes against main; `lint-imports` 10 kept,
  0 broken; `ty` clean.
- Real machine (a registered Linux machine with Claude Code signed in,
  node reinstalled with this tree): process A dispatched a 25 s task,
  saw the prompt out after 2.4 s and the record keep up with the frames
  (6, then 10), then stopped the way the gateway stops 5.4 s after the
  dispatch, leaving the record and the agent running there; process B,
  started fresh, found the record and the running task, picked it up
  in 2.4 s, announced the agent's reply to the conversation with the
  machine note, finished the record, removed the run's record, held the
  agent's process as the pool's connection, and closing the pool ended
  the agent on the machine. This revision, the cli lane with the code
  before and after, the node reinstalled for each: a cli entry
  `sh -c 'echo $$ > pid.txt; sleep 20; echo done; cat'` was dispatched
  and the host stopped the gateway's way 2.7 s later. Before: no
  record, the command left running on the machine, and the next start
  found nothing to pick up. After: the record kept with the task as
  authored and the manager's origin, the command running on, and the
  next start picked it up in 16.8 s (the sleep), announcing `done` and
  `hi` with the instance, node and tool call and the machine note. The
  same with the link killed before the stop: recorded, left, picked up.
  With the machine's daemon ended between the stop and the pick-up, it
  ended its groups on its way out, the task's record there said exit
  -9, and the pick-up announced that as the run's error in 0.1 s,
  leaving nothing on the machine.
- `scripts/check_source_language.py`, `scripts/check_commit_messages.py`
  and `scripts/check_large_files.py` on the branch range, `commitlint`,
  `ruff` -- all clean.

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

The English and Chinese agent-collaboration guides explain normal
startup,
registered-machine setup and recovery, and the removed trial switch.

## Risk

Configured machine entries are available by default; entries without a
`machine` remain local. A remote-looking file path is resolved against
the
registry even without an environment variable; use an explicit local
path
such as `./a:b` for a local filename containing a colon. A formerly
hidden
machine entry is now available for dispatch, and an old variable set to
`0`
no longer disables it. Stopping Raven no longer cancels a turn
running on a machine; it goes on there until the agent finishes or is
cancelled, and its answer arrives in the conversation after the next
start, as a sub-agent result that triggers a turn. One record per run in
the raven home, owner-only, removed when the run is concluded; a machine
that cannot be reached keeps it, so a run is reported at most once and
never lost quietly. The adopted connection answers requests with ids
past the ones the stopped process used, so a replayed frame resolves
nothing sent after the restart. Rollback is a plain revert; a record
left
behind is then an inert file.

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

## Related Issues

#920 and #922 (merged); #921 was carried by #922.

---------

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
Handsome-wzw added a commit that referenced this pull request Oct 15, 2026
…ough its node (#947)

## Summary

Adds MCP lending for remote ACP agents on top of the machine and
recovery
services already merged in #920, #922 and #934. The branch includes
current
main and its default-on registered-machine behavior.

What this does: an acp entry on a registered machine is handed the MCP
servers its row grants, the way an entry on this computer is, and the
servers themselves stay here with their definitions and keys.

- The agent on the machine runs the node's own bridge as a stdio MCP
  server (`python -I -m raven.node --mcp --dir <node dir> --task <task>
  --lend <dispatch> --server <name>`, `raven/node/bridge.py`): two byte
  pumps around a connection to the daemon's socket, no MCP parsed, no
  credential held, the standard library only.
- The daemon carries the bridge's frames as a *stream* to the host
  holding the task (`mcp/open`, `mcp/frame`, `mcp/close`; protocol 6,
`raven/node/protocol.py`, `raven/node/server.py`). The daemon tells
which
  task a bridge is by the process that connected -- the peer pid off the
socket (`SO_PEERCRED` on Linux, `LOCAL_PEERPID` on macOS), walked up the
parent chain to the agent the daemon started, which the bridge is under
and no other task's process is; the kernel supplies that pid and a peer
cannot forge it. `mcp/open` carries only `{task, lend, server}`, and the
task it names must be the one the connection is a process of. Two tasks
on
  one machine run as one user, so no name or file a bridge could present
would tell them apart; the connecting process does. One stream per
bridge, at most 16 per task; a frame
  travels in pieces of at most 256 KiB characters, the last marked
(`protocol.pieces`), up to the 32 MiB the host's own bridge allows; the
  daemon relays and keeps nothing. A stream ends with either end, the
  host's link or the task, and the other end is told.
- The host opens the endpoints for a machine row exactly as for a row
here
(`McpEndpoints`: disabled tools withheld on the wire, stdio upstreams in
the host's sandbox, HTTP auth added here), binds them to the machine,
dispatch and acquired agent task
(`NodePool.lend`/`unlend`) before handing out bridge stanzas. On
`mcp/open`
the host checks the daemon-authenticated task against that binding
before
  connecting; it rechecks the grant and link after the socket opens. An
unlend, replacement grant or replaced link cannot authorize that pending
connection. The host then pumps the frames both ways
(`raven/node/client.py`).
`node/hello` names the bridge; a node serving without a daemon
(`--stdio`)
  names none, and the dispatch then goes without MCP and says so.
- The reply closes with the servers lent and the machine's node, or with
why none were. Remote lending requires Unix-socket support on the host:
on Windows it is withheld before endpoint setup, the task continues, and
the reply explains why. The roster's "injectable MCP" matches that
support
  (`session_mcp_delivered`). The cli lane on a machine still
  withholds its servers (the next change); the openai lane has no tools.

Not in this change: the cli lane's MCP file on the machine; a stream
that
survives the link going (a bridge sees its server end, the agent reports
the call failed, and the next dispatch works again); MCP for a run
picked
up after this computer restarted (#934): it reports its calls fail.

## Type

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

## Verification

Follow-up fix 05eaba6, after the c9de539 review:

- The nine tests the Windows review named now pin the host fact through
one
  fixture, `unix_socket_host` (`tests/conftest.py`), which sets
  `raven.mcp.bridge.unix_sockets_available` to true and re-points every
  `raven.*` module that imported it by name; their bodies are unchanged.
- Found complete, not just the nine: the whole suite was run with
  `asyncio.start_unix_server` and `asyncio.open_unix_connection` deleted
before collection (a pytest plugin, controller and xdist workers), at
the
  merge base `6b938a426` and at `c9de53947`, and the two failure sets
compared. 17 node ids failed at the head and not at the base: the nine,
and eight this change added that run the transport for real (the bridge
module, and two lending tests through the stand-in machine). Those eight
are guarded by host capability instead of pinned: the bridge module
skips
when `sys.platform == "win32" or not unix_sockets_available()`, the two
lending tests carry the existing `skipif(not unix_sockets_available())`.
After the fix the same run fails the same 66 node ids at the head as at
  the base: nothing this change adds or touches fails on a host without
  unix sockets.
- A bridge whose daemon side goes ends as streams end. The host can
close a
  stream while a frame of the agent's is on its way; the daemon writes
`mcp/close` and hangs up, and the frame meets the closed socket, or the
  read side is reset first. That exited 1 with `BrokenPipeError` or
`ConnectionResetError`; a connection-level end is now exit 0 with
nothing
said, a frame past the cap still exits 1 with its reason, both pumps are
  awaited before the socket closes, and a daemon gone before the stream
opens is one sentence, not a traceback. Four deterministic cases with a
  stand-in connection fail on `c9de53947` and pass here.
- The bridge test's stand-in agent outlives its bridge, as an agent
outlives
  one of its MCP servers, so the later parts of

`test_a_stream_the_host_closes_ends_the_bridge_and_a_bridge_of_no_task_or_of_another_s_never_begins`
no longer race its exit: 1 failure in 80 runs on `c9de53947`, 0 in 80
here.
- The focused set (node, MCP, remote, manager and roster files) -- 935
  passed; `.venv/bin/python -m pytest -n 6` -- 31602 passed, 3 failed
(`test_rpc_files.py::test_a_host_without_libreoffice_says_so` and the
two
  simulation cases, which fail the same way on main on this computer);
  diff coverage against main 97.67% (420/430). Real machine, node
reinstalled with this tree in a directory of the check's own: a stdlib
ACP
  agent's bridge reached a host-held server and 1 MiB and 8 MiB results
  crossed whole, bridge exit 0; nothing was left on the machine.

Follow-up fix c9de539, after the same-user token finding:

- The daemon no longer uses a token. `agent/launch` mints and writes
none,
  `mcp.token` and the `hmac` check are gone, and `Daemon.open_stream`
  identifies the bridge by the connecting process instead (`_peer_pid` +
  `Daemon.task_of`). The same-user path the reviewer kept open -- a peer
reading another task's `mcp.token` and opening its stream -- is closed,
  because identity is the connection the kernel reports, not a readable
  secret.
- Tests, each failing on the unfixed tree first: a bridge placed under
task
  B that names task A is refused (`the connection is a process of task
  'agent@box-2', not 'agent@box-1'`), as are a process under no task, an
  unplaced connection, and a task the daemon does not run
(`tests/test_node_server.py`); a real-OS test launches an agent, takes
its
real child, and asserts the child resolves to the task while the test's
own
  process resolves to none (runs on Linux in CI); `_peer_pid` is checked
against a real socketpair; the in-process and subprocess bridge tests
now
identify by the connecting process, and a bridge this test spawns
itself,
  or another task's agent spawns, never opens the stream.
- Windows withholding now covers a row on this computer too, not only a
  machine row: `session_mcp_delivered` drops the `machine and` guard and
answers no whenever `unix_sockets_available()` is false, so a local acp
row is no longer advertised as injectable and no longer raises at
dispatch
on a host without the transport; `_mcp_endpoints` yields nothing before
opening an endpoint and `_mcp_note` explains it. The two tests the
Windows
review named now guard on `unix_sockets_available()`, and the
withholding
  test is parameterized over a machine row and a local row.
- `scripts/coverage_gate.py diff --base-ref origin/main --threshold 90`
over the node/MCP/remote suite -- 97.63% changed-line coverage
(412/422).
  `ruff`, `ty` on the changed modules, and the import-cycle budget pass.
- Real machine (ravenx2, this head, node reinstalled, digest
fdbe132e76f0):
a stdlib ACP agent's bridge reached a host-held MCP server and a 1 MiB
and
  an 8 MiB tool result crossed whole (`CALL_OK`, bridge exit 0), so the
  connecting-process identity admits the legitimate bridge end to end on
  real Linux; the cross-task refusal on Linux is the real-OS test above,
  which CI runs there.

Earlier review fixes (4dc5b3e, the task-to-grant binding). Its
host-side
binding of each dispatch's endpoints to the acquired agent task is kept;
the
daemon-side token this run verified was since replaced by the
connecting-process
identity above (c9de539), so the token-named tests and reproduction
below no
longer describe the current tree.


- `uv run --no-sync pytest tests/test_node_client.py
tests/test_node_server.py tests/test_node_bridge.py
tests/test_node_relay.py tests/test_node_bundle.py
tests/test_node_install.py tests/test_subagent_acp_remote.py
tests/test_subagent_cli_remote.py tests/test_acp_per_session_mcp.py
tests/test_subagent_acp_mcp_lifecycle.py tests/test_mcp_endpoint.py
tests/test_subagent_mcp_grant.py tests/test_mcp_bridge.py
tests/test_acp_remote.py tests/test_remote_file_tools.py
tests/test_subagent_probe.py tests/test_subagent_acp.py
tests/test_import_cycle_budget.py tests/test_resume_remote.py
tests/test_remote_resume.py tests/test_subagent_manager.py
tests/test_subagent_third_party.py -n 4 --idle-ceiling-strict
--cov=raven --cov-branch --cov-report=json:<coverage-json> --cov-report=
--tb=short` -- 1495 passed and one minimal-node stdio response test
failed;
  strict idle accounting also flagged the assembly test. Both cases pass
together under `-n 0 --idle-ceiling-strict` (2 passed). No assertions or
timing limits were weakened. The new authorization, revocation and host
  compatibility tests all pass.
- The five focused cases failed on the unfixed implementation first: a
  different authenticated task selecting another grant, a stream with no
task, each missing Unix socket API, and an astral Unicode chunk over the
node's byte limit. Regressions also cover revocation, grant replacement
and link replacement while an endpoint connection is opening, and a real
  remote turn continuing when both host Unix socket APIs are absent.
- `uv run --no-sync python scripts/coverage_gate.py --coverage-json
<coverage-json> diff --base-ref origin/main --threshold 90`
  -- 97.94% (381/389 changed executable lines); this fix alone is 100%
  (40/40). Python lint, type, dependency
  and import checks pass; all 10 import contracts hold. The scope checks
  for commit messages, source language and file sizes pass. Both RPC
  generators pass `--check` after integrating current main.
- A real Linux node with two concurrent tasks and a host loopback
endpoint:
  before, the attacker's own task token plus the victim's public grant
  names returns the victim endpoint's marker; after, it returns no data
  and the endpoint is never reached. Its own grant works, unknown and
revoked grants fail, and the same attack remains denied after the relay
  is killed and an ordinary ping reattaches both tasks. A full chunk of
  262144 astral characters was lost/corrupted before and arrives intact
after. The final documented source tree was reinstalled and checked
again.
- Real ACP dispatch through the node's bridge reaches a host-held stdio
MCP tool and returns its marker; its grant is removed when the turn
ends.
  With the host Unix socket APIs removed to simulate Windows, the remote
  agent still completes, receives no MCP, explains the host limitation,
  and the host MCP ledger records no extra call. This is not a native
  Windows run. Dedicated test tasks, host endpoints and bridge processes
  are cleaned up.
- CONTEXT.md, module/field docstrings, both collaboration guides, the
  external design notes and this description were checked against the
  implementation. Main's no-debug-switch behavior and full-turn recovery
  documentation are retained.

Full combined-tree verification: `uv run --no-sync pytest -n 4
--tb=short
--cov=raven --cov-branch --cov-report=json:<full-coverage-json>
--cov-report=`
returned 31594 passed, 230 skipped, 2 xfailed and 4 failed. Each failure
reproduces on current main in a separate checkout: host CLI color
rendering,
LibreOffice detection and the two simulation cases. No other test fails.
The minimal-node response test that failed in the focused parallel
selection
passes in this full run. Diff coverage: 97.94% (381/389 executable
changed lines).

Earlier author verification:

- `uv run pytest tests/test_node_bridge.py tests/test_node_server.py
tests/test_node_client.py tests/test_node_relay.py
tests/test_node_bundle.py tests/test_node_install.py
tests/test_subagent_acp_remote.py tests/test_subagent_cli_remote.py
tests/test_acp_per_session_mcp.py
tests/test_subagent_acp_mcp_lifecycle.py tests/test_mcp_endpoint.py
tests/test_subagent_mcp_grant.py tests/test_mcp_bridge.py
tests/test_acp_remote.py tests/test_remote_file_tools.py
tests/test_subagent_probe.py tests/test_subagent_acp.py
tests/test_import_cycle_budget.py tests/test_resume_remote.py
tests/test_remote_resume.py tests/test_subagent_manager.py`
  -- 1182 passed. Covered: on the daemon, `node/hello` naming the bridge
  and a stdio node naming none, the token written owner-only beside the
  record, a stream opened with the task's token and refused with another
  or for a task not running, refused with no host holding the task,
  refused past 16 per task, one per bridge connection, the pieces of a
  frame crossing both ways as they are and a stranger carrying nothing,
  and every way a stream ends (the bridge closing, the host closing, the
  bridge's connection ending, the host's link ending, the agent ending)
  with the other end told; the bridge as a process against a real daemon
  over its socket: a frame to the host and one back whole, a large frame
  each way in pieces, the agent closing its stdin ending the stream and
the bridge leaving clean, the host closing the stream ending the bridge,
  a wrong token and a missing daemon each refused on stderr with exit 1;
  the stand-in machine with the real daemon and the real bridge: an acp
stub there that connects to its `mcpServers` and calls a tool reaches a
  stdio server here and answers with its result, the stanza handed to it
  naming the node's bridge, the task, the dispatch and the server and
  nothing of the server's definition or socket, the ledger here showing
  one call, no bridge outliving the dispatch; the reply's note and the
  roster predicate for a machine row.
- `uv run pytest` on this tree -- 31288 passed, 230 skipped, 2 xfailed,
4 failed:
  `test_rpc_files.py::test_a_host_without_libreoffice_says_so`,

`test_simulation_scenario.py::test_a_fresh_trial_refreshes_the_subagent_homes_before_it_runs`,

`test_simulation_suite.py::test_a_stopped_run_is_marked_in_its_record_and_finished_with_its_evidence`
and
`test_subagent_kimi_code.py::test_an_ask_that_runs_out_takes_what_its_kimi_started`,
which fail
  the same way on main on this machine.
- Real machine (a registered Linux machine with Claude Code signed in,
  the node reinstalled with the code before and after):
  A28, before (the
  code of #934's head and its node): the turn closed with "MCP servers
  'probe' were not lent", Claude Code said no tool of that name was
  connected, and the server here saw no call; after: `PROBE_OK:r2-lend`
  in 9.4 s, the server here recorded the call, and the turn closed with
"lent to acp agent ... through the Raven node on machine ...". A34:
after
the turn the machine's process list shows no bridge, a grep of its node
  directory finds nothing of the server's command or of this computer's
  socket directory, and no endpoint socket is left here. A29: with the
tool switched off here, Claude Code answered UNAVAILABLE and the server
saw no call. A30: a 40 s tool call under way, the bridge visible in the
  machine's process list, then cancelled: 3 s later no bridge there and
  no socket here. A31: the relay ssh killed during a 20 s call: the turn
ended 4.1 s later with "CALL_FAILED ... Connection closed", and the next
dispatch reached the server again in 7.6 s. A32: five calls in one turn
  all reached the server and came back; an 8 MiB answer: the server here
  produced it, and a plain ACP agent on the machine (a stdlib stub that
speaks the stanza's bridge directly) received it whole through the node
-- 1 MiB in 1.3 s, 8 MiB in 3.5 s, the bridge exiting 0 -- while Claude
  Code itself closed the server on that answer and reported the call
  failed: the agent's own cap on a tool result, not the link's.
- `ruff`, the commit-message and PR-body checks -- clean.

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

The English and Chinese collaboration guides describe lending,
task-bound authorization,
Windows withholding, and the existing CLI/restart limitations.

## Risk

Registered-machine support is enabled by configuration, without the
removed
trial environment switch. An ACP entry on a machine receives its granted
MCP servers on a Unix-socket-capable host; the host's normal MCP policy
still
applies. This intentionally exposes those tools to the selected remote
agent.
The server definition, credentials and endpoint paths remain on the
host.

The daemon identifies a bridge's task by the connecting process (the
peer
pid the kernel reports, walked to the agent it started), not by any
token or
name the bridge presents, so a peer task sharing the OS account cannot
open
another task's stream; the host then authorizes that task against the
dispatch/server grant. A system that reports no peer pid opens no
stream.
On Windows hosts remote lending is withheld explicitly, without aborting
the
remote task. Protocol 6 remains the MCP wire version; host and node must
also
have the same code digest, so updated nodes must be reinstalled.
Rollback is
a revert; the machine then carries no stream and receives no bridge
stanza.

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

## Related Issues

#934 (merged); #920 and #922 (merged).

---------

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
Handsome-wzw added a commit that referenced this pull request Oct 16, 2026
…nce, a dead-link cancel (#957)

## Summary

Seven edges of a run on a registered machine, left by the review of #934
and by timing probes on the real machine. Each is a fix the host or the
node makes on its own; nothing changes for an entry without `machine`.

- **A Raven node killed outright no longer leaves agents behind or a
bare `exit -1`.** The next daemon brings every record left at `running`
to `lost` at its start (`Daemon.recover`), ends the agent when its pid
is still the process it was started as (group leader, the recorded
command, and the start mark the launch recorded: Linux's `/proc` start
tick, elsewhere `ps -o lstart`), and writes why; a record without the
mark, or a pid whose mark differs, is left alone and recorded as gone.
`agent/exit` and the attach answer carry that `reason`; the host hands
it on in place of the exit code (`ChannelEndpoint.exited(rc, reason)`),
so a cli run says "the Raven node on this machine was restarted while
the agent ran, and the agent is gone" and a picked-up acp run says the
same. On Linux the agent is also tied to the daemon's life
(`PR_SET_PDEATHSIG`, a SIGTERM). Protocol number unchanged: the field is
optional and old nodes never send it.
- **A failed remote run's error survives a draining-refused announce.**
The manager's two error branches read whether the announce was taken, as
the reply branch does (`announced = taken is not False`), and the acp
backend keeps the record of a prompt that went out and failed, the cli
backend that of a timed-out command, for the caller that reports it. The
next start reads how the task ended off the machine and announces the
error.
- **A cli run picked up after a restart binds its instance to the
session id in its output** (`_bind_picked_up`), as the dispatch's
deferred commit would have; with no id in the output the reply carries
the same "not resumable" warning the dispatch would. A `provisioned` id
or a stateless run binds nothing.
- **A cancel while the machine's link is known dead returns at once.**
`_cancel_turn` reads `ChannelEndpoint.offline` (parked, or the link not
alive), marks the session unsettled and leaves the held `session/cancel`
for the next link, instead of waiting the settle budget for a settlement
that cannot arrive. In the gap before the heartbeat notices nothing
changes.
- **An openai row's tunnel gives up a taken local port for another.**
The port is checked again just before ssh starts (`_still_free`), and
ssh's own "Address already in use" / "Could not request local
forwarding" ends in another try (`_TUNNEL_TRIES` = 3) rather than in a
request sent to whoever took the port; each attempt reads only what ssh
wrote in its own run of the log.
- **A run record that is not JSON, or not UTF-8, is set aside once**
(`<name>.json.unreadable-<ts>`), named in the log that once, instead of
being warned about at every start; a record a later version wrote is
left alone as before. (The first revision read the record as text and
let a `UnicodeDecodeError` out of the whole scan, found by the Windows
review; the record is read as bytes now and decoded by `json.loads`, so
a decoding failure is set aside like a JSON one, and one bad record
never stops the others from being read.)
- **A path written as remote that names no registered machine** is
refused in one sentence for `write_file` and `edit_file` too, never
taken for a relative path here. This has been the case since the debug
switch went (#934); a regression test pins it.

## Type

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

## Verification

Every fix has a test that failed on the old code first.

- `.venv/bin/python -m pytest -n 6 -q tests/test_node_server.py
tests/test_node_client.py tests/test_remote_runs.py
tests/test_subagent_manager.py tests/test_subagent_cli_remote.py
tests/test_remote_file_tools.py tests/test_subagent_acp_remote.py
tests/test_resume_remote.py tests/test_remote_resume.py
tests/test_subagent_acp.py` - all green on macOS (the Linux-only pact
test skipped there); the last run of the four fast files 331 passed, cli
remote 52, remote file tools 24, the acp remote recovery tests 3
- Full suite: 31577 passed, 234 skipped, 2 xfailed in 6 min 14 s; the 3
failures are the ones `main` has on this machine (a host without
LibreOffice, two simulation records). Changed-line coverage: 98.18% of
165 executable changed lines (`scripts/coverage_gate.py diff --base-ref
origin/main`); the 3 uncovered lines are the Linux-only `prctl` import
block, which CI's Linux shards run.
- `ruff check` / `ruff format --check`, `ty check` on the changed
modules, `lint-imports` (10 contracts kept),
`scripts/check_commit_file.py`, `scripts/check_source_language.py`,
`scripts/check_large_files.py`: clean.
- Real machine (ravenx2, an isolated registry copy and Raven home per
tree; the node of each tree installed under its own `node_dir`; the same
probe script on this branch and on `main`):

| Probe | `main` | this branch |
|---|---|---|
| P1 `kill -9` the daemon under a running cli turn | 4.9 s after the
kill: `CLI agent 'x' exited -1`; the loop the daemon had started was
still running (1 left) | 5.1 s: `CLI agent 'x': the Raven node on this
machine was restarted while the agent ran, and the agent is gone`; 0
left; a new daemon pid |
| P2 an acp prompt that fails after it went out, `reported_by_caller` |
the prompt failed (`exit 5`); no record left | record kept; the next
start announced the error with `exit 5` and removed the record |
| P3 cancel while the link is known dead; the cancel on the next link |
the cancel returned after 2.35-2.46 s, held until the link was back (a 2
s backoff in the probe; up to the 5 s budget in production); it landed
on the next link (task frames 3->4) | 0.00 s; landed the same way (3->4
within 0.1 s of the restore) |
| P4 the chosen local tunnel port is taken by a listener | the tunnel
used the taken port; the request timed out against the listener that
held it | the next port; HTTP 200 through the machine |
| P6 a derived-id cli create left running, picked up by the next start |
picked up ok, no binding (`commits=[]`) | picked up ok, the handle bound
to `edge-123` |

After the merge of `main` (which brought #947's MCP streams, #953's
host-death sweep and #963's install fixes into the same files): the
related groups (node server/client/bridge, remote runs, resume, cli
remote, acp remote, manager, remote file tools, acp client, import-cycle
budget) 1006 passed; diff coverage 97.30% of 185 changed lines (the 5
uncovered are the Linux-only `prctl` lines, run by CI's Linux shards);
the real-machine probes re-run on the merged tree: 8/8 PASS (P1 5.2 s, 0
orphans; P2 record kept and the error announced; P3 0.00 s and the
cancel landed on the next link; P4 the next port, HTTP 200; P6 bound to
`edge-123`).

Docs checked against the code before pushing: CONTEXT.md's Raven node
entry (records of failures, set-aside records, the daemon's recovery and
the `reason`), the module docstrings of `remote.tunnel`,
`remote_runs.pending`, `Daemon.recover`, `_still_ours`, `_death_pact`,
`ChannelEndpoint.exited/offline`, `_bind_picked_up`,
`_track_on_machine`, the `agent/exit` comment in `protocol.py`, and the
CHANGELOG entry.

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

## Risk

- [x] Security impact considered - the daemon ends only a pid that is
still the process it started: group leader of its own group, running the
recorded command, and started when the record says (`born`, written at
the launch); a reused pid fails the last mark even for the same program
and is left alone, the record then saying the agent is gone. No new
network surface; the `reason` is the node's fixed sentence, never user
or agent text.
- [x] Backward compatibility considered - `agent/exit`'s `reason` is
optional, protocol number unchanged; a host on this branch reads an old
node's exits as before, an old host ignores the field. Records written
before this branch read unchanged.
- [x] Rollback path is clear for risky changes - a plain revert; nothing
is migrated.

## Related Issues

Follow-up to #934 (the two non-blocking notes of its review) and to the
timing probes run on #921/#922/#934.

---------

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
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.

4 participants