Repository navigation
feat(*): a Raven daemon on a registered machine, reached over one watched ssh link - #920
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>
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: the node installer must use uv instead of pip.
I reviewed the full github/main...HEAD diff and covered the repository rules (AGENTS.md, CLAUDE.md, CONTEXT-MAP.md, and the Raven node additions to CONTEXT.md), affected ACP/node/file-path callers, the single-commit history, backward compatibility of the generalized ACP endpoint, test changes, and the documented layer-seat constraints. I found no weakened tests.
Verification after uv sync --all-extras --all-groups:
uv run pytest tests/test_acp_client_teardown.py tests/test_acp_remote.py tests/test_cli_ops_connection_commands.py 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_connection_add.py tests/test_ops_remote_path.py tests/test_ops_transport.py -q- 443 passed, 4 skipped because/bin/tcshand/bin/cshare unavailable.uv run pytest tests/test_subagent_acp.py tests/test_acp_protocol.py tests/test_acp_journal.py -q- 365 passed.- Focused Ruff, source-language, large-file, commit, and diff-whitespace checks passed (the
makeexecutable is unavailable, so the two repository check scripts were invoked directly).
The initial focused pytest attempt exercised no tests because the fresh environment lacked the local raven_everos development package; synchronizing the declared extras/groups resolved that setup issue.
One blocker is marked inline.
|
Windows compatibility review: two findings. Scope: complete 34-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' node-ssh
# 12 remote-command arguments instead of 1; native producer/parser mismatch
uv run pytest 'tests/test_node_bundle.py' 'tests/test_ops_remote_path.py' -n 0 -x --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\pr920\node-tests-pytest'
# 93 passed, 1 skipped (symlink privilege)
uv run pytest 'tests/test_node_client.py' -n 0 -x --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\pr920\client-tests-pytest'
# 24 passed, 29 skipped (POSIX-shell fixtures)
uv run pytest 'tests/test_node_server.py' 'tests/test_node_relay.py' -n 0 -x --collect-only --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\pr920\collect-pytest'
# collection failed: ModuleNotFoundError: fcntl; exit 1
uv run pytest 'tests/test_acp_client_teardown.py' -n 0 -x --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\pr920\teardown-pytest'
# 1 failed: signal.SIGKILL absent; exit 1
uv run pytest 'tests/test_acp_client_teardown.py' -k 'without_process_groups or platform_is_read' -n 0 -x --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\pr920\windows-teardown-pytest'
# 2 passed, 2 deselectedThe argument probe uses the real |
|
Blocking: 1 finding needs correction before this revision can merge; it is the first of the notes below. Scope: none of this PR's 34 files is under Notes:
Verification:
Checked and deliberately not reported:
|
LivXue
left a comment
There was a problem hiding this comment.
Requesting changes: 1 blocking finding, with 5 nonblocking notes and 1 question, is in the PR-level summary comment. None is filed inline, as no file in this PR is under raven/agent/**, the part of the tree I review; the summary also carries the scope statement and the verification log.
…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>
…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>
|
Thank you for the two reviews. Every point is in the commits now on this branch; the uv-only installer is the commit before them.
Windows: |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
Reviewed the new-head delta and its integration with the full PR diff, callers, and history. The earlier uv-only blocker is fixed: node setup now uses uv venv and uv pip install, obtains a private uv when possible, and refuses installation when uv cannot be obtained. The host-safe SSH argv, owner-only socket/records, off-loop whole-file work, in-flight call accounting, Windows collection guards, import-budget coverage, and remote-path documentation changes also match the failures previously reported.
One nonblocking test-portability issue is marked inline. It is introduced and reproducible under an ordinary umask 0002, but it affects only the assertion, has the umask 0022 workaround, and does not leave an operator without a working product path, so it does not meet the round-four blocker bar.
Verification: the affected suite produced 462 passed, 4 skipped, 1 failed on Python 3.12.3; the sole failure is the inline umask assumption. The isolated case failed the same way on Python 3.13.2 and passed under umask 022. ACP regressions passed (365). Ruff, all 10 import contracts, source-language and large-file gates, and git diff --check passed. make is unavailable in this environment, so I ran the two canonical gate scripts directly with their documented revision range.
Coverage included AGENTS.md/CLAUDE.md and CONTEXT routing, the diff and new commits, callers, history, backward compatibility, test weakening, and the stated architecture boundaries. The Windows module skips occur before POSIX imports and do not skip those tests on POSIX.
…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 delta from the previously assessed head. The explicit chmod(0755) fixes the only outstanding suggestion under both tested umasks, and splitting the binary/text os.fdopen calls preserves runtime behavior while satisfying the type-check overloads. I found no new issue.
Verification: the isolated ownership test passed under umask 0002 and umask 022; all 90 node-server tests passed; the previously affected 12-file suite passed with 463 tests and 4 environment skips; focused Ruff and ty check raven/node/server.py passed; git diff --check passed.
Coverage for this revision included the new diff, its own_file callers, commit history, backward compatibility, test-strength preservation, and the already-reviewed AGENTS.md/CLAUDE.md, CONTEXT terminology, and architecture constraints. This delta changes no dependency, domain term, public interface, or architectural edge.
|
Windows compatibility re-review: the previously reported SSH argv and POSIX-import/teardown failures are addressed in the exercised paths; one test expectation needs updating. Scope: all 35 changed files at P2 - Update the unknown-machine fixture for Windows drive-relative paths. Reproduced. tests/test_ops_remote_path.py:101 still expects The native launch probe confirmed that the real node preparation passes an argv list, preserves a key path containing spaces, and supplies exactly one remote-command argument to the intercepted connection. A real local file was readable through absolute, drive-relative and extended Windows spellings with the machine lane enabled. Both POSIX daemon test modules now skip before importing fcntl; the four teardown tests pass. 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_node_bundle.py tests/test_ops_remote_path.py tests/test_acp_client_launch.py tests/test_acp_client_teardown.py tests/test_acp_remote.py tests/test_node_client.py tests/test_node_install.py tests/test_node_server.py tests/test_node_relay.py -k 'not path_that_names_no_registered_machine and not each_agent_gets_an_owner_only_ssh_log' -n 0 -x --tb=short --basetemp "$R/checks/920-final-host/pytest" --junitxml "$R/checks/920-final-host/junit.xml"
# 235 passed, 58 skipped, 13 deselected. POSIX daemon/shell/install and symlink-privilege cases skipped.
uv run pytest tests/test_ops_remote_path.py -k path_that_names_no_registered_machine -n 0 -x --tb=short --basetemp "$R/checks/920-final-path/pytest" --junitxml "$R/checks/920-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_resolved_paths.py -k 'not executable_spellings' -s -n 0 -x --tb=short --basetemp "$R/checks/920-final-native/pytest" --junitxml "$R/checks/920-final-native/junit.xml"
# 2 passed, 1 deselected; scratch argv/path probes, no SSH connection.
# Exact API-base control, with source imports pinned to that snapshot:
uv run pytest tests/test_acp_remote.py::test_each_agent_gets_an_owner_only_ssh_log -n 0 -x --tb=short --basetemp "$R/checks/920-platform-control/pytest" --junitxml "$R/checks/920-platform-control/junit.xml"
# 1 failed: the same POSIX 0700 mode assertion as at the inspected head.The host selection excludes the separately reproduced path-error group and The daemon ownership/umask discussion is already resolved and is not repeated here. Remote daemon hardening was code-reviewed; no live SSH, remote installation, remote daemon execution or full suite ran. Windows skips do not establish those POSIX behaviors. |
|
No blockers; suggestions only, and they are marked inline. The new Windows evidence supersedes my earlier clean stance. I verified the reported mismatch at I am carrying this as the one nonblocking follow-up already identified in the Windows re-review; I am not duplicating it as another inline finding. The production correction is sound, and updating the fixture is straightforward. |
…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>
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 delta from the previously assessed head. Replacing the ambiguous one-letter fixture with ab:b.txt fixes the Windows expectation without weakening coverage: a:b.txt remains exercised as a Windows drive-relative local path, while the two-letter name remains an unknown remote host on both platform branches. I found no new issue.
Verification: tests/test_ops_remote_path.py passed all 91 tests; direct probes exercised both _WINDOWS branches and preserved the intended distinction; focused Ruff and git diff --check passed.
Coverage for this revision included the changed test, the parser behavior it asserts, callers and history, backward compatibility, test-strength preservation, and the already-reviewed AGENTS.md/CLAUDE.md, CONTEXT terminology, and architecture constraints. This delta changes no production code, dependency, domain term, interface, or architectural edge.
|
The fixture is updated in 7b3757c, now on this branch: the colon-in-a-name case names a two-letter host ( |
|
Windows compatibility re-review: the drive-relative fixture is corrected; one intermittent test failure remains. Scope: the complete 35-file PR at P2 - Do not require reattachment to be the last heartbeat request. Reproduced intermittently on native Windows. tests/test_node_client.py:901 checks Native Windows, Python 3.13.12; fresh run-owned environment from $R = 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\abf44f96254d43bc81b1be33079fb26b'
uv run pytest 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 -n 0 -x --tb=short --basetemp "$R\checks\920-host\pytest" --junitxml "$R\checks\920-host\junit.xml"
# 116 passed, 32 skipped, 1 failed (heartbeat assertion).
uv run pytest tests/test_node_client.py::test_a_heartbeat_unanswered_loses_the_link_tells_its_agents_and_reconnects -n 0 -x --tb=short --basetemp "$R\checks\920-heartbeat\pytest" --junitxml "$R\checks\920-heartbeat\junit.xml"
# 1 passed.
uv run pytest tests/test_node_client.py -n 0 -x --tb=short --basetemp "$R\checks\920-node-client\pytest" --junitxml "$R\checks\920-node-client\junit.xml"
# 14 passed, 29 skipped, 1 failed (same assertion).All 91 remote-path tests passed, including the corrected |
|
No blockers; suggestions only, and they are marked inline. The new Windows evidence supersedes my clean stance. I confirmed the race at I am carrying the intermittent assertion as the one nonblocking follow-up already identified in the Windows re-review, without duplicating it as my own inline finding. Production reconnection behaved correctly in the probe, and the test can assert that the expected attachment occurred without requiring heartbeat traffic to stop. |
…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>
|
Fixed in 418923f: the reconnect test looks for the attach among the new link's requests instead of reading its last request, so a heartbeat that follows the attach no longer fails it. |
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 delta from the previously assessed head. The reconnect test now searches the replacement client's requests for the exact agent/attach method and payload instead of requiring it to remain last after heartbeat scheduling resumes. That fixes the reproduced race without weakening the behavior under test. I found no new issue.
Verification: all 55 tests/test_node_client.py tests passed with xdist disabled, focused Ruff passed, and git diff --check passed.
Coverage for this revision included the changed assertion, the heartbeat/reconnect ordering and callers it exercises, commit history, backward compatibility, test-strength preservation, and the already-reviewed AGENTS.md/CLAUDE.md, CONTEXT terminology, and architecture constraints. This test-only delta changes no production behavior, dependency, interface, domain term, or architectural edge.
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; this can merge as far as I am concerned.
Reviewed the complete delta from the previously assessed head. It removes transient review-date wording while retaining the durable explanations for Windows argv handling and heartbeat interleaving. No executable behavior changed, and I found no new issue.
Verification: the directly associated ACP launch/remote and node-client suites passed with 145 tests and 4 unavailable-shell skips; focused Ruff, the source-language gate, and git diff --check passed.
Coverage for this revision included all changed lines, their documented callers and history, backward compatibility, test-strength preservation, and the already-reviewed AGENTS.md/CLAUDE.md, CONTEXT terminology, and architecture constraints. This comment-only delta changes no dependency, domain term, public interface, or architectural edge.
|
Windows compatibility re-review: the reported heartbeat-test race is fixed. No additional Windows compatibility issue was found in the inspected scope. Reviewed the complete 35-file PR at Native Windows / Python 3.13.12; fresh run-owned environment prepared with $R = 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\0605c160aa864b11bad6b7d18f24455f'
uv run pytest 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 -n 0 -x --tb=short --basetemp "$R\checks\920-host\pytest" --junitxml "$R\checks\920-host\junit.xml"
# 127 passed, 33 skipped.The complete selected node-client file passed, including the previously failing reconnect test. Skips cover POSIX-only SSH/daemon fixtures and unavailable symlink privilege. No live SSH, remote installation/daemon operation, or full repository suite was run. |
|
No blockers in the reviewed scope. Scope: this re-review covers the six commits since First-round notes, at this head:
New notes, all nonblocking:
Verification:
Checked and deliberately not reported:
|
|
Windows compatibility re-review: no additional Windows compatibility issue found in the inspected scope. The long-line paging regression's new native tests pass. Scope: the complete 37-file PR at Native Windows / Python 3.13.12, fresh per-head environment from $R = 'C:/Users/livxu/AppData/Local/Temp/raven-windows-review/d1d0b660deeb48e3a2e4d079a3316cbc'
uv run pytest 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 -n 0 -x --tb=short --basetemp "$R/checks/920-paging/pytest" --junitxml "$R/checks/920-paging/junit.xml"
# 12 passed; 1 advisory idle-ceiling warning.
uv run pytest 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 -n 0 -x --tb=short --basetemp "$R/checks/920-host/pytest" --junitxml "$R/checks/920-host/junit.xml"
# 127 passed, 33 skipped.Paging coverage includes CRLF/Unicode separators, chunk boundaries, long lines on and off the requested page, and the property that bytes are split once. Host checks cover Windows path classification, SSH argv, ACP teardown and node reconnect behavior. No native performance benchmark or remote daemon was run; skipped cases require POSIX fixtures or symlink privilege. The already recorded node/installer follow-ups remain separate. No live SSH, installation, UI execution or full repository suite. |
LivXue
left a comment
There was a problem hiding this comment.
Blocking: this full-PR review found five correctness issues (2 P1, 3 P2) in node recovery and daemon state management. The five inline comments describe the failing states and repair directions.
This review includes raven/node and supersedes any merge-readiness impression from my earlier agent-only and Windows compatibility notes. The earlier long-line paging finding remains resolved; its 12 regression cases pass.
Reviewed head: 6a0ec2c12cf4ae928de4e4b6464c85e99516937b. Verification used native Windows, Python 3.13.12, and a freshly synced isolated environment. All 4,375 blobs in the source archive were checked against the GitHub tree, and the source was checked again after the probes.
- Relevant repository tests: 338 passed, 10 failed, 61 skipped. All 10 failing test IDs also failed in the same-machine merge-base control (
829eb28e1db877dffdd3df4f835d707e1f049d3a); the comparison used JUnit test IDs, not just counts. - Controlled recovery probes: 4 failed, 1 passed. Daemon state probes: 2 failed, 2 passed. These are six violating states covering the five findings, with three positive controls.
uv run --frozen --no-sync lint-imports: 10 contracts kept.
Repository test command:
uv run --frozen --no-sync pytest tests/test_node_bundle.py tests/test_node_client.py tests/test_node_install.py tests/test_node_server.py tests/test_node_relay.py tests/test_acp_client_launch.py tests/test_acp_client_teardown.py tests/test_acp_remote.py tests/test_ops_remote_path.py tests/test_cli_ops_connection_commands.py tests/test_filesystem_tools.py tests/test_ops_connection_add.py tests/test_ops_transport.py -n 0 -q --tb=short
Verification limits: no real SSH, remote installation, or POSIX daemon end-to-end run. The socket finding uses the production path function with a fixed POSIX UID substitute; the launch-limit probe uses production request/launch logic with mocked subprocess creation and POSIX directory setup. An initial selection also included unchanged tests/test_read_file_image.py, whose unguarded os.geteuid call prevents Windows collection; that file was excluded from the completed selection, and the base has the same call.
The existing file-gate/index observations are not filed again. A separate error-redaction hypothesis was excluded because the real local uv failure did not establish the proposed credential leak.
…ry, a launch reserves its place A full review of the link's recovery and the daemon's state found five states they got wrong. Each is reproduced by a test of its own before the fix, and three of them on a real machine as well: a dead link replaced by a plain call left its agent parked for good; a cancel asked while the link was dead never arrived and the task ran on; eleven launches at once started nine against a limit of eight. A link the heartbeat had not yet seen die was replaced by the next call without its agents: the pool now attaches whatever waits for the machine on every link it makes, and the call hears why when that link goes too. A replay the new link could not carry escaped the reconnect with the frames already dropped and the endpoints after it in no list: a frame leaves the held list only once the link took it, the endpoint being replayed and every one not yet reached are parked again with what they hold, and the link is tried again. A cancel is wanted from the moment it is asked until the node takes it or says there is nothing to take; a link that dies under the request, or had died unnoticed, keeps it for the next link. The daemon's long-path socket is named for the install as well as the directory, as the socket under run/ is, so two installs on one long directory listen apart. A launch takes its place against the agent limit before its first await, so launches served together cannot pass the check as one. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
|
All five are fixed in 093966f and answered on their threads, each with the test that reproduces your probe. The three a real machine can show were run there before and after the fix, from the same script (the relay killed and a plain call made before the heartbeat could notice; a cancel asked in that gap; eleven launches at once): the run before reproduced each as you described it, the run after passes. The node test groups pass with the seven new tests, and the fix is carried to #921, #922 and #934. |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
Reviewed the delta from 6a0ec2c12cf4: ordinary link replacement now reattaches parked endpoints; replay removes frames only after delivery and reparks the remaining endpoints on failure; cancellation intent survives failed delivery; long socket fallbacks include the install identity; and launches reserve capacity before their first await. The new tests exercise each reported race or collision and do not weaken prior assertions.
I also checked the affected callers, reconnect and daemon lifecycle, relevant history and backward compatibility, repository rules, and the existing node architecture constraints. uv run pytest tests/test_node_client.py tests/test_node_server.py -q passes all 155 tests; Ruff and git diff --check github/main...HEAD pass. The previously named file-gate scope, authenticated-index argv, and invalid-index-port follow-ups remain nonblocking.
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
Reconfirmed the checked-out and live PR head are both 093966f8fa6d; the already-reviewed revision delta and its verification leave the three previously named maintainer-deferred follow-ups as nonblocking. This revision can merge as it stands.
|
Windows compatibility re-review: no additional Windows compatibility issue found in the inspected scope. Scope: all 37 changed files at Native Windows / Python 3.13.12: 144 passed, 33 skipped. The selected tests cover Windows path classification, ACP argv/teardown, bundling, file paging and the new ordinary-call reconnect, failed replay and cancellation-retention cases. Those recovery cases use controlled client substitutes. Socket fallback namespacing and launch reservations were checked in the POSIX daemon code; its Unix-socket/process-group tests do not run on this host. Exact verification commandsFresh isolated uv environment and detached checkout; source imports verified against this head, with separate home/temp/token directories. Setup succeeded: $R = 'C:/Users/livxu/AppData/Local/Temp/raven-windows-review/c281ddf96c3446c98e4f4127722b59c6'
$cases = @(
'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'
)
uv run pytest @cases -n 0 -x --tb=short --basetemp "$R/checks/920-host/pytest" --junitxml "$R/checks/920-host/junit.xml"Limits: no real SSH/remote install, POSIX daemon end-to-end execution, full repository suite or Windows ACL audit. Platform skips do not establish equivalent Windows support. |
…ackoff for the agents that wait The previous fix had a link replaced by an ordinary call bring the parked agents along, but only when the relaunch succeeded. When it failed -- the machine unreachable at that moment -- the call raised and nothing tried again: the agents stayed parked with no reconnect task, where the same failure under the heartbeat would have been retried on the backoff. On a real machine, the relay killed and the relaunch made to fail once, the agent was still parked thirty seconds after the machine was reachable again. A failed relaunch with agents waiting now schedules the same reconnect the heartbeat loss does; the agent is attached again on the first retry once the machine answers. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
|
One more commit, a6cc40d: the ordinary-call path of the first finding was fixed only for a relaunch that succeeds. A relaunch that fails with agents parked now schedules the reconnect the heartbeat loss does, so the agents come back on the backoff once the machine answers, as the module's contract says. Reproduced on a real machine before the fix (the relay killed, the relaunch made to fail once: parked = 1, no reconnect task, still parked 30 s after the machine was back) and gone after it (a reconnect task scheduled at once, the agent attached again on the first retry after the machine answered, nothing left running there); the unit test fails on the previous head and passes here. Carried to #921, #922 and #934. |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
Reviewed the two-file delta from 093966f8fa6d, the surrounding acquisition/reconnect state transitions, the added regression case, and the existing project constraints. The failed ordinary-relaunch path now schedules the same guarded backoff recovery used after heartbeat loss, while an in-progress reconnect remains unique. uv run pytest tests/test_node_client.py -q passes all 61 tests, Ruff passes on both changed files, and git diff --check github/main...HEAD is clean. The three previously named maintainer-deferred node follow-ups remain nonblocking.
|
Windows compatibility re-review at Reconciled the complete 37-file PR diff with the previous review at No additional Windows issue found in this retry change. Native host/paging checks: 145 passed, 33 skipped. The new controlled-client regression confirms that a failed ordinary relaunch schedules a retry and reattaches the parked endpoint when a later launch succeeds. POSIX daemon and stand-in shell cases remain skipped. Exact verification commandsNative Windows / Python 3.13.12, fresh isolated uv environment and detached exact-head checkout; imports verified, with separate home/temp/token directories. Setup succeeded: $R = 'C:/Users/livxu/AppData/Local/Temp/raven-windows-review/615c117f238046cca9805459d4b37a77'
$cases = @(
'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'
)
uv run pytest @cases -n 0 -x --tb=short --basetemp "$R/checks/920-host/pytest" --junitxml "$R/checks/920-host/junit.xml"
# 145 passed, 33 skippedLimits: no real SSH, remote installation, POSIX daemon end-to-end run, full repository suite or Windows ACL audit. Passing host tests does not establish equivalent Windows daemon support. |
…node # Conflicts: # raven/acp_client/client.py
|
Merge conflict: @Handsome-wzw the main branch tip has advanced to d106a5c and this PR no longer merges onto it cleanly. Re-running the merge check against the live tip: reports a content conflict in raven/acp_client/client.py (the other files auto-merge). The judge has cleared the current head, so a rebase onto the latest main, resolving raven/acp_client/client.py, is the one step left before this PR can enter the acceptance lane. Posted by the review board after re-running the merge check. |
|
Blocking: the branch must be rebased onto current main and the conflict resolved. Confirmed against live |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
Reviewed the merge revision against live main, including the raven/acp_client/client.py conflict resolution, its process and remote-endpoint callers, and the retained PR diff. The resolution preserves the node endpoint/argv work while integrating main's process-tree teardown and leftover-agent bookkeeping. The live merge check and git diff --check github/main...HEAD pass; the focused ACP lifecycle and node client/server suites pass all 242 tests, and Ruff passes on the integrated files. This settles my merge-conflict blocker. The three previously named maintainer-deferred node follow-ups remain nonblocking.
|
Windows compatibility re-review at Reconciled the complete 37-file base-to-head diff with the previous review: 29 files remain byte-identical; inspected the other 8 files against the new base and prior head, including upstream changes and the ACP endpoint/process-cleanup integration. All discussion pages and current threads were checked; No Windows compatibility issue found in the inspected scope. Native checks: 147 passed, 33 skipped. The merged endpoint path awaits Windows tree cleanup, retains the launcher's exit guard, and keeps Linux-only leftover-agent bookkeeping guarded. Tests include real native ACP startup/close and the exited-launcher case. 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'
)
uv run pytest @hostCases -n 0 -x --tb=short --basetemp "$R/checks/920-host/pytest" --junitxml "$R/checks/920-host/junit.xml"
# 147 passed, 33 skippedLimits: no real SSH/remote installation, POSIX daemon end-to-end execution, full repository suite or Windows ACL audit. Git HTTPS transport failed; GitHub's Git data API supplied missing objects, each SHA verified before checkout. This did not reduce the source diff. |
|
Not a blocker (documentation). This PR adds a CONTEXT.md Avoid entry listing "server" as a name to avoid for the Raven node concept (the canonical term is "Raven node"), but the module docstrings at raven/node/main.py:1 and raven/node/protocol.py:1 name it "Raven Subagent Server". It is docstring prose only, zero runtime -- a within-PR domain-term self-inconsistency (CLAUDE.md section 6, "use the canonical term, not a synonym"). Not a merge blocker; noted so the wording can be reconciled in a follow-up. (The module file raven/node/server.py is a legitimate code/module name; this is only about the docstring prose naming the concept.) Severity of this one finding, not a verdict on the pull request. Any blocking findings from this pass are review threads on the changed files; GitHub renders those collapsed, as a file name with no text. |
|
No blockers in the reviewed scope. Scope: this re-review covers the three commits since The five notes, at this head:
Question:
Still open, not filed against this change; the code is unchanged since
Verification:
|
LivXue
left a comment
There was a problem hiding this comment.
Approving at a9eabb049d4e: the five notes of my review at 6a0ec2c12cf4 are fixed, and the one question left is not blocking. The PR-level summary comment has how each note was re-checked and the question; this approval replaces that review's request for changes.
…ress (#922) ## Summary Third of four changes that let a Raven dispatch work to the machines in its machine registry. #920 is merged; this branch is opened against main ahead of #921, so the diff here includes #921's commits as well: review from "read, write and deliver files on a registered machine by address" on (the commits before it are #921's, carried here until it merges). What this does: - The file tools take a path on a registered machine as it is written for scp -- `user@host:/path`, or `scp://user@host:port//path` when the port is not 22 -- and answer it through the machine's daemon (`raven/agent/tools/remote_files.py`, hooked from `filesystem.py` and `file_search.py`): `read_file`, `list_dir`, `find` and `grep` run Raven's own tools there and bring back the answer, so a read of fifty lines of a large file moves those fifty lines; `write_file` and `edit_file` write there, with the digest of that agent's own last read -- the conversation's, or its sub-agent run's -- as the expected state, so a file changed since it read it is refused and nothing is written, and another agent's read does not stand in for its own. The path is resolved against the registry only, never this computer's ssh config, so one path names one machine on any computer; one that matches no row is refused, not guessed. - What is shown back as an address is only what the tool says about paths -- its errors, its headers and footers, a write's account, a diff's two header lines; a read's numbered lines, a search's matches and context, and a diff's body come back as they are, so the model edits against the bytes the file holds. The shell's refusal of typed `ssh` to a registered machine (`raven/agent/tools/shell.py`, `machine_exec.py`) reads the program in any spelling -- a path, a backslash, and on a Windows host `ssh.exe`, `SSH.EXE` or a whole `C:\...\ssh.exe`, with the backslashes the shell there keeps and without the quotes it takes off (a quoted piece and a bare one next to it making one word), so a quoted destination or port is matched like a bare one -- and, with the lane on, names the file tools. `scp`, `sftp` and `rsync` to a registered machine stay what they are on main: a copy starts nothing there and passes exec's own question like any command, and the file tools cannot carry a directory, a binary file or a large one, so the copy stays the route for those. A drive-relative or extended Windows path (`C:file`, `\\?\C:\...`, UNC) is this computer's, not a machine nobody registered. - Permissions (`raven/permissions/`): a path inside the directories Raven's agents work in on that machine -- its session directory, its `remoteCwd`, the row's listed `paths` -- is read without asking; elsewhere a read asks once per machine and directory for the session. A write there asks wherever it falls, as a write here does, and its grant is for the one file, apart from any read. The daemon checks the roots again on the machine, links included. - `deliver_files` fetches one file by its address, whole and checked, under a 20 MB cap, into `<workspace>/pulled/<machine>/<key>/<name>`, the key a digest of the file's path on the machine, so two paths never share a landing: not two files of one name from two directories, and not two names this computer's filesystem reads as one (`Report.txt` and `report.txt`, a trailing dot). The name beside the key is written as one this computer keeps (a backslash, what Windows reserves in a name, control characters and the percent sign itself as percent-codes), and a landing that would not resolve under the machine's delivery directory is refused before anything is made; a bigger file is named with its size and read there instead. Only a file under the directories this Raven's agents work in on that machine is fetched: the tool asks nothing, and a read some conversation was allowed is no evidence for this one, so a file elsewhere is refused and read there instead. Nothing reclaims `pulled/`: a delivery is opened from its landing for as long as its record lasts, and a bound by size or age is left out of this change on purpose. ## Type - [ ] Fix - [x] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification - `uv run pytest tests/test_remote_file_tools.py tests/test_permissions_remote_read.py tests/test_deliver_files_tool.py tests/test_shell_machine_channel.py tests/test_ops_remote_path.py tests/test_permissions_gate.py tests/test_import_cycle_budget.py tests/test_node_server.py tests/test_node_client.py tests/test_subagent_acp_remote.py tests/test_acp_remote.py tests/test_cli_ops_connection_commands.py` -- all passed on this tree (703 in the run that covered the first eight groups, the node's own suite and the real-node cases among them). Covered: each tool on a remote path answers as its local tool would, through a stood-in link and a real daemon; a path outside the roots is refused on both sides; a write with a stale digest is refused and writes nothing; the address form on what the tool says about paths, and a read's lines, a grep's matches and a diff's body untouched; the shell's refusal of typed ssh to a registered machine, by host, by alias, in a pipeline, as `ssh.exe` and `SSH.EXE`, and with a whole Windows path or a quoted destination or port when the host is Windows, a copy with scp, sftp or rsync not refused, and the file tools named only with the lane on; a fetched name holding the other platform's separators lands as one name under the machine's directory and nothing lands elsewhere, and a landing that would resolve outside fetches nothing; a Windows path form read here with the lane on; the read permission inside the roots and the question elsewhere, remembered per machine and directory, a write asked wherever it falls and its grant kept per file and apart from a read; a read's digest kept per agent, so another agent's read lets no stale overwrite through and an agent that never read the file expects nothing; a delivery fetched whole under a key of its path (two files of one name, two names a filesystem reads as one, and the percent-code pair all land apart), refused over the cap and refused outside the roots whatever was read before. - `uv run pytest` on this tree after the merge with main -- 31210 passed, 231 skipped, 2 xfailed, 4 failed, every one of the four in a test that needs what this machine lacks (`test_rpc_files.py`, the two simulation cases and the kimi-code case that fail the same way on main here); the groups above also pass under `--idle-ceiling-strict`, CI's idle ceiling. - Diff coverage of this change against main after the merge: 95.27% (805 of 845 executable changed lines), against the 90% gate. The node suite, the remote-path tests and the real-node cases among the groups above run on the merged tree. - Real machines (two registered Linux machines): fifty lines from the middle of a 5 MB file read through the daemon in 30 to 35 ms, byte for byte what this computer reads; list, find and grep answered; a write and an edit landed as the local tools would, and a write after the file changed was refused; a path outside the roots refused; a file written by an agent there fetched back by address. The shell's refusals are covered by the tests above. - `scripts/check_source_language.py`, `scripts/check_commit_messages.py` and `scripts/check_large_files.py` on the branch range, `commitlint`, `ruff` -- all clean. - [x] Relevant tests pass locally - [x] 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. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues #920 (merged); depends on #921. --------- 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>
…ode's own version (#963) ## Summary Three fixes to installing a Raven node on a registered machine (`raven ops connection install-node`), all in `raven/node/install.py`. The first two are nonblocking notes LivXue left on #920; the third is an older problem with the version a node names itself by. - **The index's sign-in stays off every command line.** The probe reads the package index pip is configured with on the machine, and the install handed it to uv as `--default-index <url>` inside the step's command. pip takes a sign-in inside that URL (`https://user:password@host/simple`), and the command was in this computer's ssh argv, the machine's `sh -c` argv and uv's argv; `/proc/<pid>/cmdline` there is readable by every account on the machine. The index now travels on the stdin of the two steps that run `uv pip install` (`packages`, `search`) and is read into `UV_DEFAULT_INDEX`, the variable uv reads as `--default-index`; a process's environment is readable only by its own user. A step that finds no line on stdin fails rather than install from PyPI behind the owner's back. With no index configured nothing changes: no stdin, uv's default. - **A port that is not a port is one sentence, not a traceback.** `_shown_index`, which drops the sign-in from the URL for every sentence, read `urlsplit(url).port`, which raises `ValueError` for a port that is not a number or is past 65535; `install-node` catches only `InstallError`, so the person saw a traceback. The display now cuts the sign-in from the text after the authority's last `@` (as `urlsplit` finds it) and never reads `.port`, and the look at the machine refuses such an index before anything is installed: "the package index pip is configured with on machine ... has a port that is not a number from 0 to 65535 (<url without sign-in>); correct index-url in pip's configuration there, or PIP_INDEX_URL". An empty port is the scheme's default (RFC 3986) and goes ahead. - **A node names its own version.** A node runs this code from `lib/`, not as an installed distribution, so `importlib.metadata` found no `raven` there: `raven.__version__` fell back to `0.0.0+unknown`, and with it the socket, lock and pid the daemon keeps under `run/` (`bundle.install_name()`) and the version the node reports at the handshake, which the machine listing shows -- while the node's directory is named by the host's version. The `path` step now also writes a distribution record, `lib/raven-<version>.dist-info/METADATA`, with the version the directory is named by, so the node reads the same version and names itself as the host named it. It sits outside `lib/raven`, so the digest is unchanged. This needs no change to `raven/node/server.py` or `raven/node/bundle.py`. A node installed before this keeps its old names until it is reinstalled; any change to the code changes its digest and asks for a reinstall anyway. ## Type - [x] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification - `.venv/bin/python -m pytest tests/test_node_install.py` -- 69 passed. Each new or changed case failed on main first (12 failed there): no step command names the index, its host or its sign-in, and `install()` feeds the whole URL as one line on stdin to exactly the `packages` and `search` steps (the `code` step still gets the tarball); under a real `/bin/sh` with a stand-in uv that records its argv and its `UV_DEFAULT_INDEX`, the packages step and both tries of the search package (the machine's index, then beside the public one) see the index in their environment and never in argv, and with nothing on stdin the packages step fails before uv runs; `_shown_index` drops the sign-in for a port that is not a number, one past 65535, an `@` inside the password and an IPv6 host, without raising; `plan()` refuses an index whose port is not a number or is past 65535 with the exact sentence above and runs nothing after the look, while a numeric port, no port and an empty port go ahead; a plan holding such an index still describes itself and its failures; the `path` step passes the version the install name begins with; and the one real install on this computer (a uv virtualenv of this interpreter, this code unpacked into it) now has the node report `raven.__version__` and `bundle.install_name()` equal to this computer's install name under `-I`. - `.venv/bin/python -m pytest tests/test_node_install.py tests/test_cli_ops_connection_commands.py tests/test_node_bundle.py tests/test_node_client.py tests/test_node_server.py tests/test_node_relay.py tests/test_subagent_acp_remote.py tests/test_import_cycle_budget.py -n 6` -- 334 passed. - `scripts/coverage_gate.py diff --base-ref origin/main --threshold 90` -- 100% (16/16 changed executable lines). `ty check raven/node/install.py`, `ruff check` and `ruff format --check` clean; `check_commit_messages.py`, `check_source_language.py` and `check_large_files.py` on `origin/main..HEAD` clean. - `.venv/bin/python -m pytest -n 6` on this tree -- 31716 passed, 172 skipped, 2 xfailed, 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. - Real machine (a registered Linux machine whose pip is configured with an HTTPS mirror, installed into a node directory of this check's own and removed after): the machine's mirror URL was given a made-up sign-in, a node was installed, and the process lines on the machine and on this computer were sampled every 0.3 s while it ran. With main's code: the sign-in was in a process line in 22 of 46 samples, in `sh` and `uv` on the machine and in `ssh` here; the node reported version `0.0.0+unknown` at the handshake and its `run/` held `0.0.0+unknown-<digest>.lock`, `.pid` and `.sock`. With this change: 0 of 68 samples; the node installed, answered the handshake with this code's digest and version `0.2.4`, its `run/` held `0.2.4-<digest>.*`, and its own `raven.__version__` and install name matched its directory. With the index pointed at a port nothing listens on, the install failed at the `packages` step with "Connection refused" -- uv took the index from its environment and did not fall back to PyPI -- and the sentence held no sign-in. Nothing of this check was left on the machine. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed No user-facing text changes beyond the new refusal sentence; the install's module docstring and the `Plan.index` field say where the index travels and what the version record is for. `CONTEXT.md` (the node is installed "in a directory named by the code's version and digest") was checked and still holds. ## Risk The index now reaches uv only through its environment, which only the machine user can read; nothing else about the install changes. uv has read `UV_DEFAULT_INDEX` since it gained `--default-index`, so a machine's uv that understood the flag understands the variable. A machine whose pip configuration names a port that is not a number now gets one sentence instead of an install that would fail at the packages step. The version record is a few lines beside the code, outside the digested package, so a node of this code reports the same digest. Rollback is a plain revert; a node installed with the record keeps working with older code, which does not read it. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues Follows the nonblocking notes on #920 (the install's package index on the command line, and the traceback for a port that is not a number). Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
## Summary `bind_roles_here` writes the four EverOS roles into `os.environ` directly, records them in `raven_everos.config._BOUND_HERE` and publishes that record as `RAVEN_EVEROS_BOUND`. `monkeypatch` undoes none of it: it records only what it changed itself, and `delenv(..., raising=False)` on a name that is not there records nothing. The everos backend tests have put the `EVEROS_*` variables back since #489, but not the record or the set, and the binding is not theirs alone: every runtime assembled with the memory plugin on binds through the plugin's start. A run of the whole suite with a watcher on these three things (2026-10-15) found fourteen files binding -- the design and ppt launchers, eval hooks, plugin tools, remote file tools, direct chat, onboarding, the TUI command tests. So one test's binding was the next test's environment, and which test paid depended on how the suite was dealt into shards: ten `tests/test_rpc_settings.py` save cases refused with "decided by the `EVEROS_LLM__*` environment variables" in shard 4/4 of #920 (whose tree predated #489), and nowhere else. What this does, tests only: - `tests/_everos_env.py`: `confined_everos_binding()`, a block that puts the variables (`EVEROS_ROOT` included), the provenance variable and the contents of the provenance set back when it ends, exception or not; and `forget_everos_binding(monkeypatch)`, which starts a reader with none of them and swaps the set rather than clearing it. - `tests/conftest.py`: one autouse fixture, `_no_leaked_everos_binding`, around every test, next to the other process-global restores (`_unbind_the_permission_turn`, `_no_leaked_usage_sink`). It replaces the everos backend file's own fixture, which covered the variables alone. - `tests/test_rpc_settings.py` and `tests/test_config_update_everos.py`: the two readers of `role_is_env_managed` start with no role of any kind exported, not only no embedding, through the helper. - `tests/test_everos_env_confinement.py`: the helper's contract (a binding made inside the block is undone; what was there before comes back; so on an exception; the forget half), and one slow test per representative file that, in a child interpreter started clean and running under the suite's own conftest, checks that the file leaves no variable, no record and an empty set behind. Red on the tree before this change for all three targets (the backend file left the record; the launcher case left twelve empty role variables, `EVEROS_ROOT` under a `tmp_path` that is gone, and the record). The board's own reproduction (the named everos case, then `tests/test_rpc_settings.py`, one process) does not fail on main any more because of #489's fixture; it was kept as a check and passes. Third commit, after review: the block restored the contents of whichever set the attribute held when it ended. That is the wrong object for a test that binds and then swaps the attribute with `monkeypatch`, because `monkeypatch`'s undo runs after an autouse fixture's teardown here (`_isolate_tracing_state_dir` asks for it and is set up earlier; autouse fixtures of one conftest are set up in name order): the swapped-in set got cleaned and the module's own came back with everything bound into it -- a probe handed the next test twelve keys. No test in the suite binds before it swaps, so nothing failed, but the helper's docstring claimed the reverse order. The block now keeps the object it began with and restores that object's contents and the attribute's binding to it, whatever the attribute holds by then; the docstring says why, and a test that binds, swaps with a MonkeyPatch undone only after the block, and reads a clean set is red before the change. ## Type - [ ] Fix - [ ] Feature - [ ] Docs - [x] CI / tooling - [ ] Refactor - [ ] Other ## Verification - A throwaway pytest plugin recording, per test, every change to `EVEROS_*`, `RAVEN_EVEROS_BOUND` and `_BOUND_HERE`, over the whole suite (`-n 6`): before this change 63 tests in 16 files changed them (14 binding, 2 clearing); after it, the watcher wrote nothing: no test changed them (31719 passed, same run). - `pytest tests/test_everos_env_confinement.py -n 0`: 8 passed, with and without `CI=1`; the three child-interpreter cases fail on the tree before this change with the leftovers named above. (The first push had them print the report after `pytest.main`, which on CI never returns -- the suite's conftest hard-exits the interpreter in `pytest_unconfigure` when `CI` is set -- so shard 1/4 failed on "the child did not report" while the targets had passed; `CI=1` reproduces that locally. The child now writes the report from a `pytest_sessionfinish` plugin into a file the parent reads.) - `tests/test_everos_backend.py` 191 passed, `tests/test_agents_ppt_launcher.py` 61 passed, `tests/test_rpc_settings.py` 153 passed, `tests/test_config_update_everos.py` 50 passed, each on its own; the board's reproduction order 154 passed; the fourteen binding files followed by every reader in one process, 1261 passed. - Full suite on the merged tree (`-n 6`): after the third commit, 31720 passed, 173 skipped, 2 xfailed, 3 failed -- the same three that fail on main on this machine (a LibreOffice case and two simulation cases); the related group (`test_everos_backend`, `test_rpc_settings`, `test_config_update_everos`, `test_everos_config`, `test_everos_server`, the two launcher/eval-hook files) 651 passed. - `ruff check`/`ruff format`, `scripts/check_source_language.py`, `scripts/check_large_files.py`, `scripts/check_commit_messages.py`: pass. No production code changes, so the diff coverage gate has no changed executable lines to measure. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk - [ ] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes Tests only. A test that asserted on a binding left by an earlier test would now see it gone; the full-suite run says none does. Rollback is reverting the commit. ## Related Issues N/A --------- Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
Summary
First of three changes that let a Raven dispatch work to the machines in
its machine registry on a design where the machine owns the work: a Raven
daemon runs there, the ssh is only the way to it, and an ssh that goes
mid-task does not end the task. This one puts the daemon on a machine and
keeps the link to it; nothing dispatches to a machine yet. The next change
runs acp sub-agents through it, the one after reads and writes files there
by address.
It follows #893 (the ssh line and the remote working directory) and
replaces #897 and #901, closed when the design changed: their code is in
here, reworked to the daemon.
After the review's second round, four more things hold: a page read
(
read_file, here and on a machine) reads the file a chunk at a timeand decodes only the page's own lines, so no one step holds the
interpreter for the whole file and the file is never whole in memory;
the whole-file calls on a machine go through one gate (readers
together, a writer alone, a waiting writer first), so a read never
answers with the old page and the new digest; the install reads the
index pip is configured with on the machine the way pip does, names it
in the plan and hands it to
uv pip install; and a machine without uvgets one pinned version as an unmanaged install, which writes no
receipt under the home directory and changes no PATH, while a node
directory that was there keeps its mode.
What a daemon is (
raven/node/):The host's ssh runs a relay there (
python -I -m raven.node --attach --dir <node_dir>,raven/node/relay.py): it bridges the ssh sessionto the daemon's unix socket under the node directory, starts the daemon
when none answers (
--serve, detached, its words inlog/serve.log),and leaves with the ssh. The daemon leaves on its own after ten minutes
with no connection and no running agent; the next relay starts it again,
so nothing is registered as a service and a reboot costs one connect.
It opens no network port; the socket is the registered user's own
(mode 600) in
run/(0700), or, when that path is too long for asocket, in a directory of the user's own under the temp directory
(0700, checked before use: a directory of the user's, never a link,
never another user's, narrowed when wider). The relay forwards only to
a socket of the user's and starts no daemon beside anything else; a
daemon whose socket's place holds a thing it cannot remove says so in
one line and leaves rather than listen beside it. ssh login is the
authentication.
host drives it with
AcpClient(over anEndpointseam this changeadds: a process, or a channel of a link). Protocol 3.
fs/read,list,find,grep,stat,write,edit,pull) run Raven's own file tools there, fenced to therootseachcall names, so a remote read comes back exactly as a local one would.
A write may carry
expect, the digest the file had when it was read,and is refused with
CHANGEDwhen it no longer matches. A page readwith its digest, a stat with one, a chunk and a checked write run in a
thread of the daemon's, writers one at a time under a lock, so the
loop that answers the heartbeat and forwards every agent's frames is
not held for the length of a large file.
agent/launch,frame,stderr,exit,cancel,close):the command named is started in the machine user's login environment
(captured once by the daemon itself, under
bashorzsh-lic),in its own process group, under a task id of the host's. Its ACP
frames travel on a numbered channel of the connection that holds it,
and are kept in the task's record on disk (
<node_dir>/tasks/<task>/:task.json,frames.jsonl,stderr.log; the directories 0700 and thefiles 0600, as the host's own journal is) as they arrive. A connection
that goes leaves the agent running;
agent/attachon a laterconnection replays the record from the frame the host last read and
takes the task over, and a finished task is replayed from its record
alone, even by a daemon that never ran it.
agent/cancelends thewhole process tree: the tree is read off
psbefore the first signal,so a command the agent put in a session of its own (Claude Code's shell
tool does) goes too.
task/listnames the tasks running or remembered;records are kept seven days.
node/shutdownends agents only under--stdio, the one mode that ties them to the connection (tests, and afallback).
probe/machine,probe/agent) andsession/dir, which makesa session directory and says where it is as the machine resolves it.
model, reads a config or opens a port; beside Raven's code the machine
needs uv (or curl or wget, to fetch one with), a Python 3.12 if it has
one (else uv brings one), loguru and pillow (about 55 MB, more with a
fetched uv or Python), ripgrep-bin when its index has it. A test runs
the node with nothing importable beyond the standard library, its own
code and those two packages, and asks every file call, so the next
import added under
raven/agent/toolsis caught here rather than onevery installed node.
The same code at both ends (
raven/node/bundle.py): the code is named bya digest of the package's files, and
node/hellocompares it, so adaemon of other code is refused with the command that updates it. An
install lives in
<node_dir>/<version>-<digest>.The host side (
raven/node/client.py):idle for ten minutes with no agent on it and no call on its way to it
is closed the next time any link is asked for. A heartbeat every 5 s;
one unanswered for 15 s means the link is lost.
holding what the host writes meanwhile (up to 1000 frames); the link is
tried again on a backoff (2, 4, 8, 16 s) for as long as any agent
waits; a link that comes back attaches every task again from the frame
the host last read, sends what waited and any cancel asked in between.
Only a task the machine no longer knows ends an endpoint, in one
sentence. A link closed on purpose just closes: the daemon keeps its
agents, and
tasks()on a later link lists them. The listing says"link lost: ...; N tasks continue on the machine; reconnecting".
never its address: not installed (with the install command),
unreachable, login refused, other code, a refused handshake, a call
that ran out of time, or the daemon's own last word.
The registry's package (
raven/ops/remote_path.py) tells a remote pathfrom a local one (a UNC or extended Windows path, and on a Windows host a
drive-relative one, are this computer's), resolves it against the
registry, and lends the tools the pool through one seam (
use_nodes,nodes) -- the tools must not import the node package, which importsthem.
raven/acp_client/remote.pykeeps the ssh command, the machinelookup, the posix wrapping of a remote command and the one-sentence
reading of an ssh failure from #893, and loses the launch command, the
env preamble and the session-directory ssh that #897 would have used:
the daemon does those. The ssh command is handed to the connection as an
argv, and
AcpClient.launchtakes one as it is, so no host's own grammarreads it again (a Windows host's parser split the line into twelve
words). What reaches a model is a path on the machine written the way
scp writes it, address included, because that is how a file there is
named to the tools; the way in -- the key path, ssh's options, ssh's own
messages -- reaches no config file, log line or model, and an error names
a machine by id and name only. The lane stays
debug-only (
RAVEN_DEBUG_MACHINES=1, ruled on #897): with it off nodaemon is started and no install is planned, each saying why.
Installing (
raven ops connection install-node <id>,raven/node/install.py): one look at the machine (the install, its uv ora curl or wget to fetch one with, a Python 3.12 if it has one, the free
space), then a question, then everything through uv, as the repository's
dependency rule asks: a uv fetched under the node directory from its
installer when the machine has none, a virtualenv uv makes of the
machine's Python 3.12 or of one it brings, the pinned packages with
uv pip, the code as a tarball on ssh's stdin, each step one ssh calland a failed step removing the half-made install; the node directory is
made 0700. A machine with neither uv nor a way to fetch one is refused
with the installer's address.
--dirkeeps installs elsewhere andrecords it on the row;
--pruneremoves other versions. Nothing else onthe machine changes.
raven ops connection doctorreports the registryas before.
Also:
CONTEXT.mddefines the Raven node and seatsraven.nodeinner inthe import contract (
pyproject.toml,lint-imports); the repo layouttable gets its row; the gateway, the TUI and the RPC entrance close the
node pool with the ACP pool on shutdown.
Type
Verification
uv run pytest tests/test_node_bundle.py tests/test_node_server.py tests/test_node_client.py tests/test_node_relay.py tests/test_node_install.py tests/test_acp_client_teardown.py tests/test_acp_remote.py tests/test_ops_remote_path.py tests/test_cli_ops_connection_commands.py tests/test_ops_connection_add.py tests/test_ops_transport.py tests/test_subagent_acp.py tests/test_acp_client*.py tests/test_cli_gateway*.py-- 859 passed on this tree; diff coverage of this change's lines from
these groups alone passes the 90% gate. Covered, beside what feat(*): run a Raven node on a registered machine to answer file calls there #901's
tests covered (parity of every file call with the tool's own answer,
the fence, each malformed field, the handshake, the stdio loop, the
install's scripts and one real install on this computer, through uv):
link and another user's directory refused; wider bits narrowed); a
daemon refused its socket's place says so and leaves; the relay
forwards to nothing that is not the user's and starts no daemon
beside it; a task's records are 0700 and 0600; a ping is answered
while a file's digest is taken; a call on its way keeps its link;
the ssh argv reaches the connection as it is and
AcpClient.launchtakes an argv; the installer fetches uv when the machine has none
(curl or wget) and refuses a machine with neither; a drive-relative
path is this computer's on a Windows host and scp's elsewhere, an
extended or UNC path this computer's anywhere; the node answers
every file call with only its requirements importable.
a second connection attaches from the frame the host had not read and
hears the exit; a finished task is replayed from its record by a
daemon that never ran it; a record left "running" by a daemon that
died reads as lost; an attach for a task nobody knows is refused by
code; the task list and the sweep of old records; shutdown from a host
leaves a daemon's tasks running.
the first, the daemon leaving when idle and leaving at once when
another serves the same directory;
--serveand--attachneed--dir.sshthat hands the line to a real loginshell and starts the real relay and daemon: a request made while the
link is down is answered after the link is back, on the same daemon;
a daemon told to stop takes its agents and the turn says so; a link
closed on purpose leaves the agent for the next link to list, attach
and cancel; a heartbeat lost parks the endpoint and the reconnect
attaches it again; a task the machine no longer knows ends its
endpoint in one sentence; an attach the link cannot carry parks every
waiting endpoint again; held frames are capped; a file fetched whole
and checked, and refused outside the roots.
bridges bytes both ways, reaches the same daemon again without
starting another, and gives up in one sentence when none comes up.
is read off
ps; the daemon's own group is never signalled.uv run pytest --collect-only-- no import error;lint-imports--10 kept, 0 broken. The daemon's two test modules skip on win32 before
importing it (fcntl, unix sockets), and the two POSIX teardown tests
lend a SIGKILL where the platform has none.
uv run pyteston the final tree of the three changes, which includesthis one -- 29217 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_runsand
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. The groups above
also pass under
--idle-ceiling-strict, CI's idle ceiling.Real machines (the final tree of the three changes, two registered
Linux machines, a scratch registry and
RAVEN_HOME):with the machine's curl under the node directory, then a virtualenv
of the machine's own Python 3.12, 16 s in all; on the machine whose
home disk was full, under
/tmp, with a Python uv brought, 11 s.Both nodes answered the handshake with this code's digest.
session/dir11 to 18 ms;agent/launch13 ms; a request's round trip 12 ms.killed mid-turn, the link read as lost in 5 s, the turn finished 27 s
after the kill with the agent's reply, 25 of 25 lines written, the
same daemon on both sides of the gap; a cancel left 0 processes 3 s
later; three sessions at once answered in 8 s; with every link closed
the daemon stayed with its records and no agent process remained.
The second round's four notes (
tests/test_filesystem_tools.py tests/test_node_install.py tests/test_node_server.py tests/test_node_client.py tests/test_read_file_image.py-- 337passed, 100% diff coverage of that commit's lines): a page is the
lines
splitlinesgives whatever the chunk edge, over every break itknows; a 120 MiB file pages with every ping answered within 0.25 s; a
read with a slowed digest beside a write of the same file answers with
the old page and the old digest and the write lands after; a waiting
writer goes before new readers; the probe reads
pip.confandPIP_INDEX_URLas pip does under a real shell, the plan names theindex and the install hands it to uv; the uv step fetches one pinned
version unmanaged; a node directory that was there keeps its mode and
one the install makes is owner-only. On a registered Linux machine
with the node reinstalled: a page read of a 1 GiB file there beside
pings every 50 ms kept the longest wait of a ping at 0.34 s; the reads
sent during a write all answered with a page and a digest of one
version; the probe found the mirror that machine's pip is configured
with; an install into a fresh node directory, forced down the fetched
path, put uv 0.13.0 under it unmanaged in 24 s, with the receipt under
the home directory untouched and the directory at 700.
The full review's five findings (
tests/test_node_client.py tests/test_node_server.py tests/test_node_relay.py-- 223 passed, theseven new tests failing on the code before the fix, diff coverage of
that commit's lines past the gate): a link the heartbeat has not yet
seen die, replaced by an ordinary call, brings its parked agents along
(
agent/attachon the new link, nothing left parked); a replay the newlink cannot carry keeps every frame it did not take, parks the endpoint
being replayed and every one not yet reached, and the link is tried
again; a cancel asked on a dead link, or failing on the new one, stays
wanted until a link delivers it, and a refusal clears it; two installs
on one long node directory get sockets apart; a launch still capturing
the login environment is counted, so a second one at the limit is
refused before the first spawns. On a registered Linux machine, one
script run before and after the fix (the local relay killed with the
heartbeat kept out of the way, then a plain call; a cancel asked in
that gap; eleven launches at once against the limit of eight): before,
the agent stayed parked and never answered, the cancelled task ran on
with its record saying running, nine launches started and ten tasks ran
there; after, the call took 0.3 s and the agent answered through the
new link, the task ended within the reattach with its record saying
so, eight started and three were refused with eight running, and
nothing was left running on the machine. The other half of the first
finding, a relaunch that fails under the call with agents parked: on the
machine, the relay killed and the relaunch made to fail once, before the
agent was still parked 30 s after the machine was reachable again and no
reconnect was under way; after, the reconnect the heartbeat loss
schedules runs here too, the agent was attached again on the first retry
after the machine answered and answered through the restored link.
scripts/check_source_language.py,scripts/check_commit_messages.pyand
scripts/check_large_files.pyon the branch range,commitlintonthe commit and the prospective squash header,
ruff-- all clean.Relevant tests pass locally
Relevant lint / type checks pass locally
User-facing docs or screenshots are updated when needed
The command describes itself in
--help; user docs follow with thechange that runs sub-agents on a machine.
Risk
Opt-in and inert: nothing starts a daemon unless the debug lane is on and
a caller asks for a machine, and an install runs only when the owner asks,
after saying what it will do. On a machine it adds one directory under
~/.raven-node(or the row'snode_dir): the install, arun/with thesocket, lock and pid of this install, a
tasks/with the records, alog/; removing the directory uninstalls it. The daemon runs as theregistered user, listens on a socket in a directory only that user can
open (and the relay forwards to a socket of that user's only), reads and
writes only inside the roots each call names, and leaves when idle. A
task's record holds the agent's ACP frames, which may quote what the
agent read: it is the user's alone (0700, 0600) and is removed after
seven days. The way in stays in the ssh argv, in memory; no error names
the address, and a model sees it only as part of a path on the machine,
written the way scp writes it. Rollback is a plain revert; an installed
daemon is then an inert directory that leaves once idle.
Related Issues
Follows #893; replaces #897 and #901.