Repository navigation
feat(*): read, write and deliver files on a registered machine by address - #922
Conversation
…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>
gloryfromca
left a comment
There was a problem hiding this comment.
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/tcshunavailable).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, andscripts/check_commit_messages.pyviauv 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.
|
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
Native Windows / Python 3.13.12, 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 deselectedExact 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 offNo 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. |
…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>
|
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 |
…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
left a comment
There was a problem hiding this comment.
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 untouchedtests/test_cli_theme.py::test_bold_accent_renders_styled_not_bareANSI/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.
|
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
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 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. |
|
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:
|
…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>
|
All three are on the branch now (a010ad8 for the two runtime ones, 7b3757c for the test).
|
|
No blockers; this can merge as far as I am concerned. Confirmed at |
gloryfromca
left a comment
There was a problem hiding this comment.
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.
|
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
Native Windows / Python 3.13.12, isolated user/Raven/temp directories, run-owned uv environment, $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. |
|
No blockers; suggestions only, and they are marked inline. I independently reproduced both boundary cases at These are valid follow-ups, so my previous |
…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>
|
Both are on the branch now (598cc8d). A percent sign in a fetched name is written out as |
|
No blockers; this can merge as far as I am concerned. Confirmed at |
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
left a comment
There was a problem hiding this comment.
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.
… 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>
|
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.
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
left a comment
There was a problem hiding this comment.
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.
|
Windows compatibility re-review at 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.
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 resultsPython 3.13.12; isolated detached head, fresh uv environment, verified source imports, separate home/temp/token paths. Setup: $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 passedCoverage 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
left a comment
There was a problem hiding this comment.
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 againstd106a5c6ef32.
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
left a comment
There was a problem hiding this comment.
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.
|
Windows compatibility re-review at 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; 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.
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 commandsPython 3.13.12; detached exact-head checkout, verified source imports, fresh uv environment and isolated home/temp/token paths. Setup succeeded: $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 passedLimits: no real SSH/remote installation, POSIX daemon end-to-end execution, full repository suite or Windows ACL audit. The new |
…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>
|
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 |
gloryfromca
left a comment
There was a problem hiding this comment.
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.
|
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: 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. |
|
Blocking: rebase onto current main and resolve the integration conflicts. Independently confirmed against live |
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>
|
No blockers in the reviewed scope. This supersedes my summary at 2ff2c78, which opened Scope: I reviewed only the What I examined: the changes since 2ff2c78 -- the copy refusal removed from Confirmed findings: none in this round. My previous round's findings, at this head:
These were measured at 6970b45. The files they rest on are unchanged since, except The notes I filed on #921, at this head:
The three findings of the Windows review on this PR ( Verification:
Checked and deliberately not reported:
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. |
|
Rebased the integration: 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. |
|
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:
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
left a comment
There was a problem hiding this comment.
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.
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>
…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>
…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>
…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>
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:
for scp --
user@host:/path, orscp://user@host:port//pathwhen theport is not 22 -- and answer it through the machine's daemon
(
raven/agent/tools/remote_files.py, hooked fromfilesystem.pyandfile_search.py):read_file,list_dir,findandgreprunRaven's own tools there and bring back the answer, so a read of fifty
lines of a large file moves those fifty lines;
write_fileandedit_filewrite 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.
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
sshto a registered machine (raven/agent/tools/shell.py,machine_exec.py) reads the program in any spelling -- a path, abackslash, and on a Windows host
ssh.exe,SSH.EXEor a wholeC:\...\ssh.exe, with the backslashes the shell there keeps andwithout 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,sftpand
rsyncto a registered machine stay what they are on main: a copystarts 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 thiscomputer's, not a machine nobody registered.
raven/permissions/): a path inside the directoriesRaven's agents work in on that machine -- its session directory, its
remoteCwd, the row's listedpaths-- 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_filesfetches 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.txtand
report.txt, a trailing dot). The name beside the key is writtenas 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 itsrecord lasts, and a bound by size or age is left out of this change on
purpose.
Type
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.exeandSSH.EXE, and with a wholeWindows 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 pyteston 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 simulationcases 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.pyand
scripts/check_large_files.pyon 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.
Related Issues
#920 (merged); depends on #921.