Repository navigation
feat(*): say the next step on a ready machine and connect an agent from its sheet - #1007
Conversation
…d used once A machine whose account had no key of this computer's yet could not be registered from the page: the sheet took a key path and nothing else. The sheet now offers a login pick, key or password. With a password, Raven makes its own ed25519 pair under the home's keys directory, logs in once through OpenSSH's askpass to append the public line to the machine's authorized_keys, and stores only the key path; every later connection is a key connection, and the password is never stored, logged or echoed. A refused password also names the other cause, an sshd that does not accept passwords; Windows is refused up front; no ssh-keygen is the no-ssh refusal the page already knows. Machines is now the hub's third module beside Agents and Channels, instead of a filter tab inside the agents page. The side rail keeps its one entry and remembers the module opened last. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
The Windows review of EverMind-AI#1005 found the refusal living in the install step, so machines.add had already run ssh-keygen (a pair left under the home) or had already found it missing (a 'nossh' asking for OpenSSH, which cannot make the mode work there). The password path now judges the platform first, before the fields, the key and the lookup, and answers with a kind of its own, 'platform', so the page explains the pick rather than the address or a missing ssh. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
The second Windows review of EverMind-AI#1005: the four password-flow cases stand the key making and the login in for but leave the host alone, so on Windows the early platform refusal answers before their stand-ins are reached and each one tests that refusal instead of its own scenario. They now run as a supported host; the native Windows refusal cases stay as they are, and so does the refusal itself. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…om its sheet A registered machine whose node is up left the owner with nothing to do: the card said "Link up, no agent" and the Agents tab did not mention the machine, while the one way to connect an agent on it (the preset sheet's Runs on) is withheld for a preset this computer already runs and absent for one not installed here. machines.list rows now carry agents_here, the connected entries that run on the machine; the card says so, and the state line reads "no agent running" so the two facts stay apart. A ready machine's sheet says the next step and offers "Connect an agent on this machine": the preset, its working directory there, Connect -- the existing subagents.add with the name fixed to <preset name>@<machine id>. subagents.presets (new, read only) lists the presets a node can start whether or not an entry holds them, with where each already runs. The "not installed" sheet offers Runs on too. machines.local_keys (new, read only) names the private keys this computer has, and the register form fills its key path from them instead of a placeholder. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
|
Full suite at e08ec8c (before the merge of main): 32664 passed, 5 failed, 176 skipped in 6m16s. The 5 are this machine's known local baseline (two mirasim ui-cli timing cases, the libreoffice host check, two simulation cases) and fail the same way on main here. After merging main (39a2a9c): the related groups again, 784 passed; ui-web extAgents + page gates 466 passed; tsc, eslint, gen:check, lint:i18n, lint:rpc clean. |
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: keep a completed machine-side connection from taking over a drawer the reader has since left.
I found one reachable async-lifecycle defect inline. I reviewed git diff github/main...HEAD, the surrounding machine/agent RPC and UI callers, the branch history including the merge from main, the repository rules and CONTEXT-MAP.md / runtime / web vocabulary, wire/backward-tolerance mappings, and the tests for weakened coverage; git diff --check is clean.
Verification:
uv sync --all-packages(needed because the initial environment lacked theraven_everosworkspace package)uv run pytest tests/test_ops_connection_add.py tests/test_rpc_machines.py tests/test_rpc_subagents.py -q- 254 passednpm test -- --run src/features/extAgents/ExtAgentsPage.test.tsx src/features/extAgents/store.test.ts src/state/hub.test.ts src/components/HubHead.test.tsx- 175 passednpm run type-check- passednpm run gen:check- generated contract matches, 247 methods
…ader has left The machine sheet's connect form awaits subagents.add, whose readiness ping can take a minute while the drawer stays closable. The completion then reopened the machine's sheet over whatever the reader had moved to, and a refusal was written to one shared field, so it landed on another machine's form opened meanwhile. Each opening of the form is now a generation, and the write in flight carries the one it was pressed in. When it ends, the busy mark is cleared only if it is still this write's, a refusal is written only to the form it was pressed in, and the machine's sheet is put back only when that form is still the one open. A write that lands after the reader left is told in a toast; a refusal that arrives then is told in one line too (new string gui.machine.connect_bad_named) instead of on the form that replaced it. Three page cases cover the switch-to-another-machine, the refusal-after-replacement and the real close path; the first two are red on the previous commit. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: make the machine-connect generation stale when the drawer closes, not only after its fade completes.
I re-reviewed the full github/main...HEAD change and the new 39a2a9c1cef2..HEAD fix, including the repository rules and context vocabulary, the shared drawer/store architecture, callers and history, wire/backward compatibility, and whether coverage was weakened. The switch-to-another-form cases are fixed, but the original close race remains during the drawer's 260 ms fade; I replied on the existing thread and reopened it.
Verification:
uv run pytest tests/test_ops_connection_add.py tests/test_rpc_machines.py tests/test_rpc_subagents.py -q- 254 passednpm test -- --run src/features/extAgents/ExtAgentsPage.test.tsx src/features/extAgents/store.test.ts src/state/hub.test.ts src/components/HubHead.test.tsx- 178 passednpm run type-check- passednpm run gen:check- generated contract matches, 247 methodsnpm run lint- passed with four existing warnings in untouched filesgit diff --check 39a2a9c1cef2..HEAD- clean
…closes The close path flips the drawer's flag at once and drops the island's card a fade later, so for 260 ms `mconnect` still named the form and a write ending in that window counted as current: it reopened the drawer the reader had just closed, or wrote its refusal to the fading form. `sheetDismissed`, which every close path runs in the same tick, now bumps the form's generation and clears its busy mark, and "still current" also asks the drawer whether it is open and ours. The close cases release the write inside the fade rather than after it; both fail on dc6dd71. 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.
The immediate-close race is fixed: the form generation is invalidated synchronously on close, the completion check also requires the drawer to remain open and owned by the agents island, and the success/refusal tests now settle inside the fade. I rechecked the full github/main...HEAD change and this revision's delta against the repository rules and context vocabulary, affected callers and shared-drawer architecture, history, backward compatibility, and test integrity; I found no further issue worth raising. My original thread is resolved.
Verification:
uv run pytest tests/test_ops_connection_add.py tests/test_rpc_machines.py tests/test_rpc_subagents.py -q- 254 passednpm test -- --run src/features/extAgents/ExtAgentsPage.test.tsx src/features/extAgents/store.test.ts src/state/hub.test.ts src/components/HubHead.test.tsx- 179 passednpm run type-check- passednpm run gen:check- generated contract matches, 247 methodsgit diff --check dc6dd71f9cd0..HEAD- clean
…t name The machine is reached by more than one route -- the chat's raven_config, the preset's own sheet, the machine's sheet -- and they spelled the default name differently, so the owner's setup could end up with one agent on one machine twice, two rows the roster cannot tell apart. The add checked only the name. subagents.add without a name now refuses a preset the machine already carries, judged on preset and machine before any ping, naming the entry there and whether it is connected or switched off; a named add is a second entry on purpose. The page's connect form leaves the name to the server's default rather than fixing a spelling of its own, so the one rule holds whichever way the add arrives; the fixture refuses the same way. Two cases in the rpc tests fail with the handler change stashed. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
|
6ea0149 adds one rule the owner asked for after walking through the page: subagents.add without a name now refuses a preset the machine already has (judged on preset + machine, before any ping, naming the entry there and whether it is connected or switched off); a named add is still a second entry on purpose. The page's connect form leaves the name to the server's default, @, instead of fixing a spelling of its own, so the chat and the page can no longer put one agent on one machine twice between them. Docs (machines guide en/zh, section 4 and the hub section), CHANGELOG, CONTEXT, the machine param description and the raven_config hint say so. Two new rpc cases fail with the handler change stashed; related groups 928 passed; ui-web 4028 passed with tsc/eslint/gen:check clean; mkdocs --strict clean. |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
The duplicate-prevention rule is consistent across the server, chat tool, page, fixtures, and tests: unnamed machine adds use <preset>@<machine>, reject an existing preset/machine pair before the readiness ping, and preserve explicitly named second entries. I found only the nonblocking contract-description mismatch inline. At this fourth round it is not a blocker: the implementation remains usable and the operator has the returned add name; the issue is that generated API documentation describes the old naming.
I reviewed the full github/main...HEAD diff and the 80e3e5266263..HEAD delta, repository rules and context terminology, affected callers and history, backward compatibility, generated wire artifacts, and whether tests were weakened. My earlier thread remains resolved.
Verification:
uv run pytest tests/test_rpc_subagents.py tests/test_rpc_machines.py tests/test_ops_connection_add.py -q- 256 passednpm test -- --run src/features/extAgents/ExtAgentsPage.test.tsx src/features/extAgents/store.test.ts src/features/extAgents/AgentsBody.test.tsx src/state/hub.test.ts src/components/HubHead.test.tsx- 203 passednpm run type-check- passednpm run gen:check- generated contract matches, 247 methodsgit diff --check 80e3e5266263..HEAD- clean
…the config, and is taken back The install script appended the public key straight after whatever authorized_keys already held, so a file whose last line had no newline took the key inside that line: unreadable itself, and the owner's own key unreadable too when no comment field absorbed it. The first registration was then refused as a login failure, and the retry wrote a second line under a damaged first. The script now puts the missing newline first, as ssh-copy-id does. The probe that proved the new key read no ssh config at all (-F /dev/null), while the login that installed it had read the config a moment before: a machine reached through a jump host or an alias took the key and then refused to register, every time. The probe now reads the config the way the login did and keeps only the agent out (transport OWN_IDENTITY, probe keep_config). And the key this computer made had no life of its own: remove takes it back from the machine's authorized_keys over one last key login and deletes the pair here, saying so either way, and a registration refused after the login says the key stays on the machine and is reused. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
# Conflicts: # ui-web/src/rpc/generated.ts
…b' into feat/machine_next_steps # Conflicts: # raven/ops/connection_add.py
|
Merge conflict: @Handsome-wzw, this PR no longer merges onto |
… own key first, add once Four things LivXue's review draft found on the machine next steps, all reproduced: machines.list read the agents section for every row and caught only the pydantic error, so a config.json that does not parse at all (a comment, a trailing comma) failed the whole listing with internal_error while the gateway itself ran on; the reader of the agents section is the one to say so, and the machine is still what it is. A real unparseable file is the test now. The register form proposed id_ed25519 before id_rsa and called that ssh's order; ssh -G lists id_rsa first on OpenSSH 8.9, 9.6 and 9.9, so an owner with both was proposed the key ssh itself would try second. The table follows the measured order, and a test pins it against the ssh of the computer running it. A row that exists, switched off, with its command missing here, was offered "Connect on <machine>" from its sheet, and the press sent the row's own toggle, which carries no machine: only a preset not yet on the roster is offered one, as the other "Runs on" already did. An add still running on a machine was not known to a form opened again on that machine, so the preset could be pressed a second time: a second proof prompt, then the duplicate refusal, with a toast saying connected and a form saying not. The adds in flight are kept by the name each will land under, and the form greys that preset. The subagents.presets description in the contract still named entries <preset name>@<machine id>; it says the add's default <preset>@<machine id> now (the review bot's note). Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
|
Blocking: rebase or merge the current GitHub main and resolve the generated RPC client conflict. I independently reproduced the verifier's result against |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
Reviewed 0e978b3cf204 against github/main and the delta from 6ea01494e50b, including the generated-client conflict resolution, the incorporated machine registration/password path, and the follow-up fixes. The synthetic merge is clean. I covered the repository rules and relevant Runtime/Web UI context, the full target diff and changed callers/history, backward compatibility of the RPC additions, test changes for weakening, and the UI-to-RPC/source and generated-contract architecture boundaries.
Verification:
uv run pytest tests/test_ops_connection_add.py tests/test_ops_transport.py tests/test_rpc_machines.py tests/test_rpc_subagents.py tests/test_cli_ops_connection_commands.py -q: 311 passed.- extAgents Vitest groups: 197 passed.
- ui-web TypeScript and
gen:check: clean; generated contract matches 247 methods. - ui-web ESLint: 0 errors, with four pre-existing warnings outside this diff.
git diff --check github/main...HEAD: clean.
The prior contract-wording suggestion is fixed and its thread is resolved; I found no remaining finding worth raising.
…in with it every time The 2026-10-19 decision replaces the mode this branch introduced (a password used once to install a key of Raven's): the password itself is what Raven logs in with, every time. It is kept on this computer under `<home>/keys/<id>.password` (0600, the owner's alone; `connection_add .store_password`), and every ssh to such a machine -- the probe, the node install's steps, `remote.push`, the per-turn tunnel, the durable node link and the tools' runner -- reads it through OpenSSH's askpass hook (`transport.ssh_env`: `SSH_ASKPASS` names a 0700 helper that cats the file named by `RAVEN_MACHINE_PASSWORD_FILE`, `SSH_ASKPASS_REQUIRE=force`). The password is on no command line, in no registry row, log or answer; the row carries `auth: password` and no key; `held_secrets` scrubs it from model output like a provider key. Such a row gets no `Host <id>` alias in `~/.ssh/config`. A password the machine refuses at registration is not kept; a registered password that is gone is refused by name before any ssh starts; removing the machine deletes it. Windows refuses the mode first, before anything is kept (its OpenSSH has no askpass). The key mode is untouched. Gone with the old mode: `generate_key`, `install_public_key`, `revoke_public_key`, `own_key`, `forget_key` and the remote scripts. Page: the register form's password note says the password is kept here and used on every connection; the machine sheet gains a Login field (key, or password kept on this computer) with the no-alias note for a password machine; the removal confirm says the password goes with the row. CLI: `raven ops connection add --auth password --password-stdin` (or the hidden prompt), `list` marks `(password)`, `remove` says the password is deleted. Contract: `MachineRow.auth`; `machines.add` descriptions. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
|
No blockers in the reviewed scope. Scope: I reviewed only the What I examined: the one changed line in scope, the The four points
Observations outside the reviewed scope:
Verification:
Checked and not reported: two unnamed adds of one preset on one machine at nearly the same moment (the chat and the page) both pass the new check, and the later one is refused as a duplicate name after its own ping, so no second entry is written. The two merge commits only regenerate the counts at the head of |
…what the first one carries After LivXue's review of bf3828d (filed two minutes before f004c47 replaced the password mode), the two points that survive the change: - F1: a password connection riding a ControlMaster from the owner's ssh config never authenticates itself, so a probe could "prove" a password it never sent and a password rotated on the machine would go unnoticed until the master expired. `PASSWORD_AUTH` now carries `ControlMaster=no` and `ControlPath=none`; the argv test and both guides say so. - F4: the form's password note says the first connection records the host key and then sends the password, with the terminal-first advice for a network one does not trust, and that a changed password means remove and register again. `keep_config` and `OWN_IDENTITY` had no caller left once the once-mode's probe went; they go, with the `ssh -G` test LivXue's F3 was about. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…n one is scrubbed at once Two credential-lifecycle findings from the review of f004c47: - `raven ops connection add` kept the password before the id was judged, so a re-add under an id in use that the machine refused deleted the registered row's password and left the row. The id is now judged before anything is kept or reached; the answer says to remove the machine first. `machines.add` already judged it in that order. - `held_secrets` stamped the password files on mtime, and an in-place rewrite inside the filesystem's timestamp resolution kept the old value in the cache, so a replacement password could reach a model verbatim. The files are a few and tiny: they are read on every call now, and only the config's credentials stay cached. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
… refused is right A password machine is refused on a Windows host (transport.ssh_env), and the seven new tests that exercise the password flow said nothing about the host they ran on, so all seven failed on a native Windows host. They now say which host they run as, through a shared fixture beside unix_socket_host; the copy this branch carried in tests/test_rpc_machines.py moves there, so there is one definition of it. The reason this branch gave for refusing Windows was wrong. Windows' OpenSSH does have an askpass hook: measured 2026-10-19 on OpenSSH_for_Windows_9.5p2, SSH_ASKPASS with SSH_ASKPASS_REQUIRE=force is honoured (ssh-keygen -p took the old passphrase from a .cmd helper), and what cannot be run is the POSIX script Raven writes -- CreateProcessW failed error:193. The refusal stands; the reason in the docstring, in the comment at it, in CONTEXT.md and in both docs pages is now the one that is true. Co-authored-by: MiniMax (minimax-m3.1-flash-free) <noreply@minimax.io>
…e the scrubber covers Four findings from 0xKT's review of ae8e3e3 and gloryfromca's reproduction of them. - The CLI kept the password before it judged anything but an exact duplicate id: `--id ../escape` wrote `<home>/escape.password` outside `keys/` (and its askpass helper over whatever sat there), `--id "My Box"` left a file `remove` cannot reach, `--id Box` on a case-insensitive disk deleted or replaced the registered machine's password, and an unreadable registry -- which `load()` reads as empty -- let all of that through. The id rule now has one owner (`connections.password_path` refuses an id `_ID` refuses), and the CLI judges the row, the registry and the duplicate in `machines.add`'s order, keeping the password only then and forgetting it on every path that ends without a write. `machines.add` forgets its own on a failed write. - A password shorter than the scrubber's floor (six) was never redacted: `root` and `admin` came back verbatim from `read_file` and `exec cat` of the password file. `store_password` now refuses one that short, with the floor imported from `raven.config.held_secrets` so the two cannot drift; both doors answer `invalid` / one sentence. - The register form's two login branches sat in one unkeyed React position, so the pick reused the DOM input and only patched its type: a password typed under Password went out as a key path and was written to the registry in the clear. Each branch is keyed now, with a case for both directions. - Seven password tests (and three in the node link) failed on native Windows, where `ssh_env` refuses the mode first of all: each now pins the host it runs as, as the RPC tests already did. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
`held_secrets` returned early when the config file was missing, before it read the machine passwords -- and those live under the Raven home on their own: a configless Raven can register a password machine, and a tool that printed `keys/<id>.password` then handed the password to the model verbatim (reviewed 2026-10-19). The absent config is now an empty config-credential set rather than the end of the call; a case with no config file pins it, and the existing one -- no config holds nothing to scrub -- still holds for the config's own credentials. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
The configless machine-password disclosure is fixed: the implementation now treats a missing config as an empty config-credential set while still scanning kept machine passwords, and the exact regression plus adjacent held-secret coverage passes (165 passed). The current head also merges cleanly with github/main.
Covered in this round: the project rules, the full diff with a focused review of the new two-file delta, affected scrubber callers, stack and commit history, backward compatibility, test strength/no weakening, and mergeability/architecture constraints. The only remaining note is the transient review metadata marked inline.
…e review AGENTS.md 1.1: a source comment carries the constraint, not the task that found it. The three notes on `held_secrets` that named the review date say their timeless reason instead -- a missing config does not imply the machine passwords are absent, and an in-place rewrite can keep the old mtime -- in the function, the branch and the new test's docstring. 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.
The cleanup removes the transient review metadata while preserving the lasting cache and configless-password invariants. The focused held-secret coverage is green on this revision (165 passed), diff checks are clean, and the head merges cleanly with the latest github/main.
Covered this round: project comment rules, the full-diff context and complete two-file delta, affected behavior and callers, history/stack context, backward compatibility, test integrity, and mergeability/architecture constraints. No tests were weakened and no further finding survived refutation.
…not a key Raven makes When this computer has no ssh key, the register form says so and points at the password login. It still said what the password mode did before EverMind-AI#1005 replaced it: "pick Password: Raven then makes a key". A password registration now keeps the password on this computer and logs in with it every time, and makes no key, so the note says that (in both languages), and the local_keys() docstring that ended on the same claim says the owner then types a path or registers with the password.
…ead its presets again When an add lands, a connect form open on that machine reads its presets again, but the in-flight mark came off before that read -- before the whole-roster read with the probe that precedes it, too. For that stretch the form opened again during the write offered the preset as free, and a press was the second add the server refuses, right after a toast said connected. The mark now comes off once the presets are back in. Reads of the presets are also counted and only the latest is written: the read the reopened form started can answer after the one the landed add asked for, and painted last it offered the preset again. Two page cases hold each read in turn; each fails without its half of the change. The note above the re-read drops the date it carried.
…a password prompt - On-call campaigns reach a password machine through trunk's runner and askpass, the docker backend and the round-zero rsync included, instead of listing it as unusable; resolve_into carries auth, and a key machine's argv is unchanged. - The askpass helper answers a prompt for the password and no other: a second factor's prompt gets nothing, since Raven cannot answer one and a PAM module may pass what it is given on to its vendor. The helper file is replaced whole rather than refilled in place, as this change rewrites it on every install. - --password-stdin implies --auth password and is refused with --auth key, where the piped line used to answer the machine's name.
… read again The note on `presets` said the form reads them when it opens; they are also read again when an add lands on its machine while it is open, and the latest read is the one kept. The note on `mconnectPending` said the mark means the write is still running; it now also stays while the form open on that machine reads its presets again after the write landed.
…b' into feat/machine_next_steps
|
@Handsome-wzw, three follow-ups on top of
On F2: yes, please do it inside this PR -- put the four files back on main's hand-wrapped text with this PR's hunks replayed on it, as you described. That includes the lines these commits add to Verified at |
…at/machine_next_steps
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
The three maintainer edits hold up: the no-key copy now describes password storage accurately, the pending marker spans the roster/preset refresh, and latest-wins preset reads prevent an older opening read from restoring a stale choice. I checked the failure and overlap paths and found no remaining behavioral defect.
Verification on this revision: extAgents/store 177 passed; backend connection/machine tests 100 passed; i18n tests 18 passed; RPC generation matches 247 methods; TypeScript and ESLint are clean. The gate group first produced an untouched boot-order timeout plus its cascading assertion (214 passed, 2 failed); the exact gate then passed 7/7 and the full unmodified group passed 216/216 on rerun. The ui-tui generator checks could not start locally because this worktree lacks the prettier and json-schema-to-typescript packages. The head merges cleanly with the latest github/main.
Covered: project rules and CONTEXT terminology, the full diff context and all three new commits, callers and asynchronous failure/race paths, history and stack shape, backward compatibility, test integrity, generated contracts, and mergeability. The only outstanding record item is LivXue’s requested F2 restoration of the four hand-wrapped files; that is nonblocking formatting work.
0e978b3 ran the root prettier config over `store.ts`, `ExtAgentsPage.tsx`, `MachineCards.tsx` and `ExtAgentsPage.test.tsx`, which ui-web does not use (476 of its 557 files are hand-wrapped and differ from it), so most of this PR's lines in those files were format noise over code main already has (LivXue). Each file is now the EverMind-AI#1005 text with this PR's own hunks replayed onto it -- a three-way merge of the base, the base formatted the way the reformat had been, and this branch's head -- with ten overlaps resolved by hand; the lines the maintainer edits added to `store.ts` and the test file came through the same way. Formatting the result with the root config reproduces the previous head except where that head itself kept hand style, so nothing changed but the wrapping; the four files' lines against EverMind-AI#1005 go from 173/211/281/1694 to 137/44/206/590, and 260 extAgents cases and the full ui-web suite pass unchanged. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
|
F2 done inside the PR, in 1834c68, on top of your three edits (merged first, clean): the four files are the #1005 text with this PR's hunks replayed onto it -- a three-way merge of the base, the base formatted the way the reformat had been, and this branch's head, with ten overlaps resolved by hand; the lines e5884dd and dac563a add to store.ts and the test file came through the same way. Their lines against #1005 go from 173/211/281/1694 to 137/44/206/590. Check used: formatting the result with the root config reproduces the previous head except where that head itself kept hand style, so nothing but wrapping changed; 260 extAgents cases (your two included) and the full ui-web suite pass unchanged, tsc and eslint clean. |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
The hand-wrapped rebuild of the four frontend files preserves the behavior added on the preceding heads, including dismissal-generation ownership, the post-add preset refresh, and latest-read-wins preset state. This also completes the previously named F2 formatting follow-up. I separately reviewed the newly stacked #1005 on-call/password transport delta and found no conflicting integration behavior.
Verification on 1834c68ee365:
- full ui-web suite: 250 files, 4,123 tests passed;
- focused extAgents/state checks: 8 files, 268 tests passed;
- affected on-call/transport Python checks: 104 passed;
- type-check, ESLint, generated RPC check (247 methods), and
git diff --check: clean; - merge-tree against the fetched
github/main: clean.
One gate run overlapped the full suite and produced five timeout/cascade failures in live-test paths; rerunning the same gate group alone passed all 49 files and 216 tests, while the full suite also passed independently.
Review coverage included AGENTS.md and canonical terminology, the complete main-to-head diff and the semantic delta from the prior head, callers and commit history, compatibility, test changes/weakening, and the relevant state/transport architecture. All threads I opened are resolved.
|
No Windows compatibility issue found in the inspected scope. Reviewed head Windows compatibility notes:
Verification and limitations:
Exact verification commands# suite: 753 passed, 0 failed, 0 errors, 39 skipped; exit 0
# Working directory: C:\Users\livxu\AppData\Local\Temp\raven-windows-review\59278fa004c7449d88c2e1059b48a106\pr1007\source\head
uv run pytest tests/test_ops_transport.py tests/test_ops_connections.py 'tests/test_ops_connection_add.py::test_local_keys_lists_the_computer_s_own_identities_in_ssh_s_order_and_a_named_pair_after_them' 'tests/test_ops_connection_add.py::test_the_default_identities_are_in_the_order_this_computer_s_ssh_offers_them' 'tests/test_ops_connection_add.py::test_local_keys_is_empty_without_an_ssh_directory' 'tests/test_ops_connection_add.py::test_a_password_is_kept_under_the_home_for_the_owner_alone_and_forgotten_on_request' 'tests/test_ops_connection_add.py::test_a_password_shorter_than_the_scrubbers_floor_is_refused_before_anything_is_written' 'tests/test_ops_connection_add.py::test_a_password_is_never_kept_under_a_path_shaped_id_and_forgetting_one_is_quiet' 'tests/test_ops_connection_add.py::test_a_row_registered_with_a_password_says_so_and_gets_no_ssh_alias' 'tests/test_ops_connection_add.py::test_the_probe_reaches_a_password_machine_through_askpass_and_the_password_is_never_on_the_command_line' 'tests/test_ops_connection_add.py::test_a_refused_password_is_one_sentence_that_names_the_other_cause_too_and_never_the_password' 'tests/test_ops_connection_add.py::test_a_password_row_with_no_password_kept_here_is_refused_before_ssh_is_started' 'tests/test_ops_connection_add.py::test_a_password_row_is_refused_on_windows_before_ssh_is_started' tests/test_rpc_machines.py 'tests/test_rpc_subagents.py::test_presets_lists_what_a_machine_can_start_whether_or_not_an_entry_holds_it_and_where_each_runs' 'tests/test_rpc_subagents.py::test_add_on_a_machine_refuses_a_preset_the_machine_already_has_before_any_ping' 'tests/test_rpc_subagents.py::test_add_on_a_machine_names_the_switched_off_entry_of_the_preset_there' tests/test_cli_ops_connection_commands.py tests/test_acp_remote.py 'tests/test_node_client.py::test_the_link_to_a_password_machine_logs_in_through_askpass_and_offers_no_key' 'tests/test_node_client.py::test_a_password_the_machine_refuses_ends_the_link_with_the_login_named_and_nothing_else' 'tests/test_node_client.py::test_a_password_machine_whose_password_is_not_kept_here_is_refused_before_ssh_is_started' tests/test_held_secrets_machine_passwords.py 'tests/test_agents_oncall_flow_connections.py::test_a_campaign_on_a_password_machine_is_told_how_it_logs_in' 'tests/test_agents_oncall_flow_connections.py::test_round_zero_on_a_password_machine_syncs_and_pulls_through_askpass' 'tests/test_agents_oncall_flow_connections.py::test_a_docker_campaign_on_a_password_machine_runs_its_commands_through_askpass' 'tests/test_agents_oncall_flow_connections.py::test_a_docker_campaign_on_a_key_machine_is_reached_as_it_always_was' tests/test_rpc_schema_match.py '--deselect=tests/test_cli_ops_connection_commands.py::test_adding_an_ssh_machine_writes_a_host_alias' '--deselect=tests/test_acp_remote.py::test_each_agent_gets_an_owner_only_ssh_log' 'tests/test_agents_oncall_flow_runner.py::test_a_key_sync_sends_what_it_always_sent' 'tests/test_agents_oncall_flow_runner.py::test_a_password_sync_lets_askpass_answer_from_a_session_with_no_terminal' 'tests/test_agents_oncall_flow_transport.py::test_a_password_machine_is_logged_in_to_through_askpass_and_offered_no_key' 'tests/test_agents_oncall_flow_transport.py::test_a_password_machine_with_no_password_kept_here_is_refused_by_name_before_ssh_starts' 'tests/test_agents_oncall_flow_transport.py::test_a_password_meta_that_names_no_connection_is_refused_by_name' 'tests/test_agents_oncall_flow_transport.py::test_a_key_machine_is_logged_in_to_exactly_as_before' -n 0 -x --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\59278fa004c7449d88c2e1059b48a106\checks\1007-suite\pytest' --junitxml 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\59278fa004c7449d88c2e1059b48a106\checks\1007-suite\junit.xml' |
# Conflicts: # ui-web/src/rpc/generated.ts
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
This revision merges current main into the branch. I reviewed the merge delta and the overlapping callers rather than treating the parent history as new branch work. The sole manual conflict, ui-web/src/rpc/generated.ts, contains both sides' methods and exactly regenerates from the combined OpenRPC contract (248 methods). The automatically merged raven/node/client.py keeps both password-machine askpass launch support and main's authenticated loopback MCP endpoint support; their combined node tests pass. The previously reviewed machine-sheet behavior remains intact.
Verification on 166939fd2234:
- focused extAgents/state/frontend checks: 8 files, 268 tests passed;
- node, machine RPC, subagent RPC, and held-password checks: 302 passed;
- TypeScript type-check and
git diff --check: clean; - ESLint: 0 errors and 4 existing hook-dependency warnings;
- generated RPC check: clean, 248 methods;
- fetched
github/mainis the merge's second parent, and merge-tree is clean.
Coverage included the repository rules and terminology, the complete branch diff already reviewed on the prior head, this revision's parent-to-merge delta, overlapping callers and history, compatibility, test integrity, and the affected RPC/node/frontend architecture. I found no new defect and all threads I opened remain resolved.
|
No Windows compatibility issue found in the inspected scope. Reviewed head Windows compatibility notes:
Verification and limitations:
Exact verification commands# suite: 755 passed, 0 failed, 0 errors, 39 skipped; exit 0
# Working directory: C:\Users\livxu\AppData\Local\Temp\raven-windows-review\449764f868914195879642f49fd4a90c\pr1007\source\head
uv run pytest tests/test_ops_transport.py tests/test_ops_connections.py 'tests/test_ops_connection_add.py::test_local_keys_lists_the_computer_s_own_identities_in_ssh_s_order_and_a_named_pair_after_them' 'tests/test_ops_connection_add.py::test_the_default_identities_are_in_the_order_this_computer_s_ssh_offers_them' 'tests/test_ops_connection_add.py::test_local_keys_is_empty_without_an_ssh_directory' 'tests/test_ops_connection_add.py::test_a_password_is_kept_under_the_home_for_the_owner_alone_and_forgotten_on_request' 'tests/test_ops_connection_add.py::test_a_password_shorter_than_the_scrubbers_floor_is_refused_before_anything_is_written' 'tests/test_ops_connection_add.py::test_a_password_is_never_kept_under_a_path_shaped_id_and_forgetting_one_is_quiet' 'tests/test_ops_connection_add.py::test_a_row_registered_with_a_password_says_so_and_gets_no_ssh_alias' 'tests/test_ops_connection_add.py::test_the_probe_reaches_a_password_machine_through_askpass_and_the_password_is_never_on_the_command_line' 'tests/test_ops_connection_add.py::test_a_refused_password_is_one_sentence_that_names_the_other_cause_too_and_never_the_password' 'tests/test_ops_connection_add.py::test_a_password_row_with_no_password_kept_here_is_refused_before_ssh_is_started' 'tests/test_ops_connection_add.py::test_a_password_row_is_refused_on_windows_before_ssh_is_started' tests/test_rpc_machines.py 'tests/test_rpc_subagents.py::test_presets_lists_what_a_machine_can_start_whether_or_not_an_entry_holds_it_and_where_each_runs' 'tests/test_rpc_subagents.py::test_add_on_a_machine_refuses_a_preset_the_machine_already_has_before_any_ping' 'tests/test_rpc_subagents.py::test_add_on_a_machine_names_the_switched_off_entry_of_the_preset_there' tests/test_cli_ops_connection_commands.py tests/test_acp_remote.py 'tests/test_node_client.py::test_the_link_to_a_password_machine_logs_in_through_askpass_and_offers_no_key' 'tests/test_node_client.py::test_a_password_the_machine_refuses_ends_the_link_with_the_login_named_and_nothing_else' 'tests/test_node_client.py::test_a_password_machine_whose_password_is_not_kept_here_is_refused_before_ssh_is_started' tests/test_held_secrets_machine_passwords.py 'tests/test_agents_oncall_flow_connections.py::test_a_campaign_on_a_password_machine_is_told_how_it_logs_in' 'tests/test_agents_oncall_flow_connections.py::test_round_zero_on_a_password_machine_syncs_and_pulls_through_askpass' 'tests/test_agents_oncall_flow_connections.py::test_a_docker_campaign_on_a_password_machine_runs_its_commands_through_askpass' 'tests/test_agents_oncall_flow_connections.py::test_a_docker_campaign_on_a_key_machine_is_reached_as_it_always_was' 'tests/test_agents_oncall_flow_runner.py::test_a_key_sync_sends_what_it_always_sent' 'tests/test_agents_oncall_flow_runner.py::test_a_password_sync_lets_askpass_answer_from_a_session_with_no_terminal' 'tests/test_agents_oncall_flow_transport.py::test_a_password_machine_is_logged_in_to_through_askpass_and_offered_no_key' 'tests/test_agents_oncall_flow_transport.py::test_a_password_machine_with_no_password_kept_here_is_refused_by_name_before_ssh_starts' 'tests/test_agents_oncall_flow_transport.py::test_a_password_meta_that_names_no_connection_is_refused_by_name' 'tests/test_agents_oncall_flow_transport.py::test_a_key_machine_is_logged_in_to_exactly_as_before' tests/test_rpc_schema_match.py '--deselect=tests/test_cli_ops_connection_commands.py::test_adding_an_ssh_machine_writes_a_host_alias' '--deselect=tests/test_acp_remote.py::test_each_agent_gets_an_owner_only_ssh_log' -n 0 -x --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\449764f868914195879642f49fd4a90c\checks\1007-suite\pytest' --junitxml 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\449764f868914195879642f49fd4a90c\checks\1007-suite\junit.xml' |
|
@Handsome-wzw, #1005 landed on main as
Verified at @0xKT: with #1005 in, the diff against main is this PR's own change. |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
This revision reconciles the branch after #1005 landed. I reviewed the merge against both parents and the three late #1005 fixes. The sole conflict in raven/ops/connection_add.py is resolved correctly: this PR's DEFAULT_IDENTITIES / local_keys() addition remains intact, while #1005's password-specific probe(..., password=...) and registration transaction behavior is preserved. The resulting comparison against current main is again this PR's own 31-file change, and none of the previously settled sheet, preset-refresh, or password-scrubbing behavior was lost.
Verification on 85a3d0d320d1:
- connection-add, transport, machine/subagent RPC, CLI, node-client, and held-password checks: 460 passed;
- focused extAgents/state/frontend checks: 8 files, 268 tests passed;
- generated RPC check: clean, 248 methods;
- TypeScript type-check and
git diff --check: clean; - fetched
github/mainis the merge's second parent, and merge-tree is clean.
Coverage included the repository rules and terminology, the current main-to-head diff, both merge-parent deltas and conflict history, affected callers, compatibility, test integrity, and the registration/transport/RPC/frontend architecture. I found no new defect, and all threads I opened remain resolved.
|
No Windows compatibility issue found in the inspected scope. Reviewed head Windows compatibility notes:
Verification and limitations:
Exact verification commands# suite: 766 passed, 0 failed, 0 errors, 49 skipped; exit 0
# Working directory: C:\Users\livxu\AppData\Local\Temp\raven-windows-review\4a301468687a4ff6be1889633af8f053\pr1007\source\head
uv run pytest tests/test_ops_transport.py tests/test_ops_connections.py 'tests/test_ops_connection_add.py::test_local_keys_lists_the_computer_s_own_identities_in_ssh_s_order_and_a_named_pair_after_them' 'tests/test_ops_connection_add.py::test_the_default_identities_are_in_the_order_this_computer_s_ssh_offers_them' 'tests/test_ops_connection_add.py::test_local_keys_is_empty_without_an_ssh_directory' 'tests/test_ops_connection_add.py::test_a_password_is_kept_under_the_home_for_the_owner_alone_and_forgotten_on_request' 'tests/test_ops_connection_add.py::test_a_password_shorter_than_the_scrubbers_floor_is_refused_before_anything_is_written' 'tests/test_ops_connection_add.py::test_a_password_is_never_kept_under_a_path_shaped_id_and_forgetting_one_is_quiet' 'tests/test_ops_connection_add.py::test_a_row_registered_with_a_password_says_so_and_gets_no_ssh_alias' 'tests/test_ops_connection_add.py::test_the_probe_reaches_a_password_machine_through_askpass_and_the_password_is_never_on_the_command_line' 'tests/test_ops_connection_add.py::test_a_refused_password_is_one_sentence_that_names_the_other_cause_too_and_never_the_password' 'tests/test_ops_connection_add.py::test_a_password_row_with_no_password_kept_here_is_refused_before_ssh_is_started' 'tests/test_ops_connection_add.py::test_a_password_row_is_refused_on_windows_before_ssh_is_started' tests/test_rpc_machines.py 'tests/test_rpc_subagents.py::test_presets_lists_what_a_machine_can_start_whether_or_not_an_entry_holds_it_and_where_each_runs' 'tests/test_rpc_subagents.py::test_add_on_a_machine_refuses_a_preset_the_machine_already_has_before_any_ping' 'tests/test_rpc_subagents.py::test_add_on_a_machine_names_the_switched_off_entry_of_the_preset_there' tests/test_cli_ops_connection_commands.py tests/test_acp_remote.py 'tests/test_node_client.py::test_the_link_to_a_password_machine_logs_in_through_askpass_and_offers_no_key' 'tests/test_node_client.py::test_a_password_the_machine_refuses_ends_the_link_with_the_login_named_and_nothing_else' 'tests/test_node_client.py::test_a_password_machine_whose_password_is_not_kept_here_is_refused_before_ssh_is_started' tests/test_held_secrets_machine_passwords.py 'tests/test_agents_oncall_flow_connections.py::test_a_campaign_on_a_password_machine_is_told_how_it_logs_in' 'tests/test_agents_oncall_flow_connections.py::test_round_zero_on_a_password_machine_syncs_and_pulls_through_askpass' 'tests/test_agents_oncall_flow_connections.py::test_a_docker_campaign_on_a_password_machine_runs_its_commands_through_askpass' 'tests/test_agents_oncall_flow_connections.py::test_a_docker_campaign_on_a_key_machine_is_reached_as_it_always_was' 'tests/test_agents_oncall_flow_runner.py::test_a_key_sync_sends_what_it_always_sent' 'tests/test_agents_oncall_flow_runner.py::test_a_password_sync_lets_askpass_answer_from_a_session_with_no_terminal' 'tests/test_agents_oncall_flow_transport.py::test_a_password_machine_is_logged_in_to_through_askpass_and_offered_no_key' 'tests/test_agents_oncall_flow_transport.py::test_a_password_machine_with_no_password_kept_here_is_refused_by_name_before_ssh_starts' 'tests/test_agents_oncall_flow_transport.py::test_a_password_meta_that_names_no_connection_is_refused_by_name' 'tests/test_agents_oncall_flow_transport.py::test_a_key_machine_is_logged_in_to_exactly_as_before' tests/test_rpc_schema_match.py '--deselect=tests/test_cli_ops_connection_commands.py::test_adding_an_ssh_machine_writes_a_host_alias' '--deselect=tests/test_acp_remote.py::test_each_agent_gets_an_owner_only_ssh_log' -n 0 -x --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\4a301468687a4ff6be1889633af8f053\checks\1007-suite\pytest' --junitxml 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\4a301468687a4ff6be1889633af8f053\checks\1007-suite\junit.xml' |
|
Not a blocker (review of 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.
|
…em from its sheet (#1019) ## Summary Reported by the owner on 2026-10-20, on the first day with #1007 on their site: the Ravenx card said "No agent connected here yet" while the machine's connect form greyed Claude Code as "already connected on this machine". Both were telling the truth by different rules. The card counted connected entries only (`agents_on_machine(only_enabled=True)`); the form, and `subagents.add`'s duplicate rule, counted every entry of the preset on the machine, switched off or not -- and the owner had just switched `Claude Code@ravenx` off. So a switched-off entry was counted nowhere on the card, blocked the form, and had no way back from the machine's side. What changes: - `machines.list` rows carry `agents_off_here`, the entries on the machine that are switched off (the connected ones stay in `agents_here`; both empty when the agents section or the config file cannot be read). The card line and the sheet field show them marked *switched off*. - `subagents.presets` now means by `on_machines` the machines a *connected* entry runs on, and adds `off_on_machines` (machine + entry name) for the switched-off ones. Both additive. - The connect form keeps greying a preset the machine has connected, but offers one it has switched off back under its own entry -- each switched-off entry as a choice of its own, by name, since a machine may hold several explicitly named ones: the option says *switched off*, the working-directory field is left out (the entry keeps the one it had), and the button reads **Reconnect**, which runs the agents page's own `subagents.toggle enabled=true` on that entry -- same proof ping, no second entry. The add is offered only while the machine holds nothing of the preset (the server refuses it otherwise). The Reconnect goes through the agents page's own `act`, so the row is marked joining for the length of the ping: that page draws it pending and cannot press its own Connect meanwhile, and the form, reading `joining` too, greys the entry the other way round -- one ping whichever side pressed. The card line and the sheet field read *N on this machine* / *Agents on this machine*, since they list switched-off entries as well. The write is guarded like the add (opening generation, drawer still current) and marked pending so a form reopened meanwhile does not offer it twice; a refusal stays on the form in the agent's words. - Docs: machines guide (en + zh), CHANGELOG, CONTEXT. ## Type - [x] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification - `pytest tests/test_rpc_machines.py tests/test_rpc_schema_match.py tests/test_rpc_registration.py -n 4`: 580 passed; `pytest tests/test_rpc_subagents.py tests/test_ops_connection_add.py tests/test_raven_config_tool.py -n 4`: 365 passed. The two extended cases (`agents_off_here` listed apart; `off_on_machines` carries the entry name while `on_machines` leaves it out) fail with the two handler changes stashed. - `ruff check` / `ruff format --check` on the changed Python files: clean. - ui-web: `vitest run src/features/extAgents scripts/gates`: 483 passed (5 new page cases: the third for two switched-off entries of one preset on one machine, each reconnectable in turn; the fourth and fifth for a toggle in flight from either side -- the agents page offers no Connect while the machine's Reconnect runs, and the form greys the entry and says why while the agents page's Connect runs -- both fail with the previous store/form; the switched-off preset is pickable and labelled, no directory field, Reconnect toggles the existing entry and lands back on the sheet with the lists re-read; a refused Reconnect stays on the form in the agent's words -- both fail with `MachineCards.tsx`/`store.ts` stashed; the card/sheet case extended with a switched-off entry). `tsc --noEmit`, `eslint` on the changed files, `gen:check` clean; `ui-tui` `lint:rpc` / `lint:i18n` clean. - `pytest tests/test_docs_site.py`: 16 passed; `mkdocs build --strict`: clean. - Full suite: see the comment on this PR. - Real page: see the comment on this PR. ## Risk - [x] Security impact considered: the new fields carry entry names already shown on the agents page; Reconnect is the existing toggle, with its proof ping and refusals. - [x] Backward compatibility considered: `agents_off_here` and `off_on_machines` are new fields (the page maps both defensively); `on_machines` narrows to connected entries, which only this page reads. - [x] Rollback path is clear for risky changes: revert the commit; nothing stored changes shape. ## Related Issues N/A (follow-up to #1007, from the owner's report on 2026-10-20) --------- Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
Summary
Follows #1005, which is now on main; main was merged in after it landed, so the diff against main is this change alone.
A registered machine whose node is up left the owner with nothing to do: the card said
"Link up, no agent" and the Agents tab did not mention the machine. The way to connect
an agent on it (the preset sheet's Runs on) is hidden exactly where it is needed most:
a preset already connected on this computer is withheld from the roster, and a preset not
installed here shows only install instructions. Measured on the owner's own setup
(2026-10-19): two machines registered from the page, both reached and with the node,
and no path on the page to put Claude Code on either.
What changes:
machines.listrows carryagents_here, the connected agent entries that run on themachine (
agents_on_machine(only_enabled=True); a malformed agents section lists none).The card says "N connected here: ..." or "No agent connected here yet"; the state line
now reads "Link up, no agent running", so "running now" and "connected here" are two lines.
A ready machine's sheet says the next step and offers Connect an agent on this machine:
a form in the same drawer with the preset, its working directory there, and Connect. It
calls the existing
subagents.addwithmachine,remote_cwdand the name fixed to<preset name>@<machine id>, so the proof ping and every refusal are the add's own.New read-only
subagents.presets: the presets a machine's node can start (acp, cli),listed whether or not an entry already holds them, with
connected_hereandon_machinesso the form greys the one this machine has and says when this computerhas it too. Built-in agents, Mirasim shims and openai endpoints are not listed.
The Agents tab's "not installed" sheet also offers Runs on when ssh machines are
registered, with a Connect button once a machine is picked: running on a machine does
not need the agent on this computer.
New read-only
machines.local_keys: the private keys this computer has, as~/.ssh/<name>(ssh's default identities in ssh's order, then any file with a
.pubbeside it). Theregister form fills the key path with the first and offers the rest, instead of a
placeholder naming
id_ed25519to an owner whose key isid_rsa; with none found itsays so and points at the password login.
One preset once per machine, held by the server whichever way the add arrives:
subagents.addwithout a
namenow refuses a preset the machine already has (judged on preset + machine, beforethe ping), naming the entry there and whether it is connected or switched off; a named add is a
second entry on purpose (two working directories, say). The page's connect form leaves the name to
the server's default (
<preset>@<machine id>) rather than fixing its own spelling, so the page anda chat can no longer put one agent on one machine twice between them (the owner's setup had the two
routes spelling the default differently, 2026-10-19).
Contract (
rpc-schema/openrpc.json), models, both generated clients andi18n/messages.jsonare updated; the offline fixture library answers the two new methods and the new field, and refuses
the duplicate the way the server does.
Docs: the machines guide (en + zh), CHANGELOG, CONTEXT.
Fifth round, after the review draft on LivXue's board (four findings, all reproduced) and the bot's inline note.
machines.listread the agents section for every row and caught only the pydantic error, so aconfig.jsonthat does not parse at all (a//comment, a trailing comma) failed the whole listing withinternal_errorwhile the gateway ran on:_agents_herenow treats the loader'sConfigReadErrorthe same way (nothing listed; the agents page says why), and the test is a real unparseable file. The register form proposedid_ed25519beforeid_rsaand called that ssh's order;ssh -Glistsid_rsafirst on OpenSSH 8.9, 9.6 and 9.9, so an owner with both was proposed the key ssh itself tries second:DEFAULT_IDENTITIESfollows the measured order, a test pins it against the ssh of the computer running it, and the guide's two sentences say so. A row that exists, switched off, with its command missing here, was offered Connect on from its sheet while the press sent the row's owntoggle, which carries no machine: only a preset not yet on the roster is offered one, as the other Runs on already did. An add still running on a machine was unknown to a form opened again on that machine, so the preset could be pressed twice (a second proof prompt, then the duplicate refusal, a toast saying connected and a form saying not): the adds in flight are kept by the name each lands under (mconnectPending) and the form greys that preset (being connected on this machine right now). The contract'ssubagents.presetsdescription names the add's default<preset>@<machine id>now, as the bot noted. The branch also carries #1005's fourth commit (merged) and main up to #1001.Sixth round: merged onto #1005's head (
9dbef0874) and LivXue's F1 fixed. The merge had three conflicts, all the same shape -- this PR's addition beside what #1005 changed (theauthfield in the machine sheet,agents_herein the fixtures, the new register assertions) -- and keeps both sides; the superseded password-used-once code (generate_key,install_public_key,revoke_public_key,forget_key, the remote scripts) is gone from the diff, so a review of this PR no longer grades that design. F1: a connect form open across a write readspresetsagain when the add lands -- whichever opening it is -- so a preset just connected on that machine reads as taken instead of inviting a second add the server refuses; the case that pinned the wrong end state moved with it. F2 (four ui-web files reformatted to the root prettier style) is withdrawn in 1834c68: each file is the #1005 text with this PR's own hunks replayed onto it (a three-way merge of the base, the base formatted the way the reformat had been, and this branch's head; ten overlaps resolved by hand, the maintainer edits' lines included), so what remains in them is this PR's change -- the four files' lines against #1005 go from 173/211/281/1694 to 137/44/206/590. Check used: formatting the result with the root config reproduces the previous head except where that head itself kept hand style, so nothing but wrapping changed.Verification, sixth round: related Python
1068 passedover the twelve files this PR touches or affects, diff coverage 96.13% (348/362 executable changed lines); full suite-n 633386 passed, 3 failed -- the LibreOffice case and the two simulation cases this Mac always fails (run before the F1 commit, whose change is ui-web only); ui-webvitest run4121 passed (250 files, gates included),tsc --noEmitandeslintclean,gen:check247 methods; ui-tuilint:i18nandlint:rpcclean; the three range gates overorigin/main..HEAD.Also merged: #1005's c0c8222 (Oncall campaigns run on a password machine through trunk's askpass; the helper answers a password prompt only;
--password-stdinimplies--auth password) and the three maintainer edits on this branch (68ea011, e5884dd, dac563a), all clean.Seventh round: two findings from the review of 91952ba. (1) A configless Raven held a kept machine password unscrubbed:
held_secretsreturned early whenconfig.jsonwas missing, before it readkeys/<id>.password-- and those live under the Raven home independently of any config, so a tool printing that file passed the password to the model verbatim (gloryfromca, inline). An absent config is now an empty config-credential set rather than the end of the call; the case is pinned with no config file at all, and the existing "no config holds nothing to scrub" case still holds for the config's own credentials. (2) CI shard 3/4 failed ontests/test_node_stderr_replay.py::test_a_reason_written_on_stderr_while_the_link_was_down_reaches_the_run_watching_it("did not happen in time", then a leftover agent in its teardown) andtests/test_subagent_cli_remote.py::test_...-- both stand-in-machine timing cases this branch does not touch, the same class as the ones recorded for #965/#979/#983; 135 passed over those three files locally in one run, and the job passes on rerun.Verification:
tests/test_held_secrets_machine_passwords.pyandtests/test_security_untrusted_context.py42 passed; the other three files that read the scrubber 223 passed;ruff check,ruff format --checkandty checkclean on the two changed files; the three range gates overorigin/main..HEAD.Type
Verification
pytest tests/test_rpc_machines.py tests/test_ops_connection_add.py tests/test_rpc_subagents.py tests/test_rpc_schema_match.py tests/test_rpc_registration.py -n 4: 768 passed (7 new:agents_hereincl. the malformed-section case,local_keysorder and exclusions at both layers,subagents.presetsmembership and placement).pytest -n 6: see the last comment on this PR for the count (run at the head).ruff check/ruff format --checkon the changed Python files: clean.tests/test_rpc_subagents.py(default-named second add refused before any ping, naming the entry and its state; named add allowed; another machine allowed; switched-off entry named), both fail with the handler change stashed; related groups 928 passed after it.vitest run4006 passed (250 files, page gates included; 7 new page cases: the card line and the sheet's next step, the connect form end to end incl. a refusal and the nothing-to-connect case, key prefill with and without keys, an absent agent connected onto a machine from its own sheet, and no machine offered when none is registered);tsc --noEmitclean;eslintclean on the changed files;gen:checkmatches the contract.gen:rpcandgen:i18nregenerated;pytest tests/test_docs_site.py16 passed;mkdocs build --strictclean.~/.ssh/id_ed25519withid_rsaas the other choice; an agent absent here (OpenClaw) connected onto the machine from its own sheet was refused in 1 s with the agent's own sentence ("the agent's command was not found on machine ...") shown in place. Gateway stopped by pid, the node directory and daemon on the machine removed afterwards.machines.listover a realconfig.jsonthat is not JSON lists the machine with no agent (test_list_lists_no_agent_over_a_config_file_that_does_not_parse_rather_than_failing; through the realDispatcherwith a scratch home and registry, the previous handler answered-32603 internal_error, this one[('box', [])]);local_keysorder at both layers, plustest_the_default_identities_are_in_the_order_this_computer_s_ssh_offers_themagainstssh -G -F /dev/null(this Mac, OpenSSH 9.9:id_rsa, id_ecdsa, id_ecdsa_sk, id_ed25519, id_ed25519_sk, id_xmss, id_dsa; the same on a registered Linux machine, OpenSSH 9.6); the page: a configured, switched-off, missing row gets no machine field and no Connect on (offers a row that exists no machine...), and a form opened again on a machine while an add still runs there greys the preset, keeps the other one pressable and sends no second add (greys a preset whose add on the machine is still running...) -- both fail with the three page sources stashed. The six Python files 821 passed, diff coverage of the changed lines 97.50%; ui-webvitest run4119 passed (page gates 216 included),tsc,eslinton the feature,gen:check(247 methods),npm run buildwith both boot goldens; ui-tuigen:rpc/gen:i18n,lint:i18n -- --check;ty checkon the changed modules;pytest tests/test_docs_site.py tests/test_living_docs.py18 passed,mkdocs build --strictclean; the three range gates; full suite (-n 6) 33353 passed, 4 failed: the three this Mac always fails (a LibreOffice case, two simulation cases) and one stand-in-machine timing case intests/test_subagent_cli_remote.py(the record not yet written when the test reads it) that passes 3 of 3 runs alone and touches no file this branch changes.Risk
machines.local_keysreturns key paths on this computer, to this computer's own page, never key material;subagents.presetscarries no credential; nothing new leaves the host. The connect form reaches the machine only through the existingsubagents.addand its proof ping.agents_hereis a new required field onmachines.listrows (the page maps it defensively); the two new methods are additive; an older page against this server ignores both.Related Issues
N/A (follow-up to #983 and #1005, from the owner's walkthrough on 2026-10-19)