Skip to content

feat(*): say the next step on a ready machine and connect an agent from its sheet - #1007

Merged
0xKT merged 32 commits into
EverMind-AI:mainfrom
Handsome-wzw:feat/machine_next_steps
Oct 20, 2026
Merged

0xKT merged 32 commits into
EverMind-AI:mainfrom
Handsome-wzw:feat/machine_next_steps

Conversation

@Handsome-wzw

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

Copy link
Copy Markdown
Contributor

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.list rows carry agents_here, the connected agent entries that run on the
    machine (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.add with machine, remote_cwd and 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_here and
    on_machines so the form greys the one this machine has and says when this computer
    has 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 .pub beside it). The
    register form fills the key path with the first and offers the rest, instead of a
    placeholder naming id_ed25519 to an owner whose key is id_rsa; with none found it
    says so and points at the password login.

  • One preset once per machine, held by the server whichever way the add arrives: subagents.add
    without a name now refuses a preset the machine already has (judged on preset + machine, before
    the 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 and
    a 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 and i18n/messages.json
are 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.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 ran on: _agents_here now treats the loader's ConfigReadError the same way (nothing listed; the agents page says why), and the test is a real unparseable file. 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 tries second: DEFAULT_IDENTITIES follows 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 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 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's subagents.presets description 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 (the auth field in the machine sheet, agents_here in 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 reads presets again 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 passed over the twelve files this PR touches or affects, diff coverage 96.13% (348/362 executable changed lines); full suite -n 6 33386 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-web vitest run 4121 passed (250 files, gates included), tsc --noEmit and eslint clean, gen:check 247 methods; ui-tui lint:i18n and lint:rpc clean; the three range gates over origin/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-stdin implies --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_secrets returned early when config.json was missing, before it read keys/<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 on tests/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) and tests/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.py and tests/test_security_untrusted_context.py 42 passed; the other three files that read the scrubber 223 passed; ruff check, ruff format --check and ty check clean on the two changed files; the three range gates over origin/main..HEAD.

Type

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

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_here incl. the malformed-section case, local_keys order and exclusions at both layers, subagents.presets membership and placement).
  • Full suite pytest -n 6: see the last comment on this PR for the count (run at the head).
  • ruff check / ruff format --check on the changed Python files: clean.
  • Duplicate-on-machine refusal: 2 new cases in 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.
  • ui-web: vitest run 4006 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 --noEmit clean; eslint clean on the changed files; gen:check matches the contract.
  • ui-tui: gen:rpc and gen:i18n regenerated; pytest tests/test_docs_site.py 16 passed; mkdocs build --strict clean.
  • Real machine, isolated gateway (own Raven home and registry copy, channels off, the branch's node installed in its own directory on the test machine), real browser: the ready machine's sheet shows the next step and the button; the form lists 12 startable presets; Connect of Claude Code on the machine landed in 9 s and the card then reads "1 connected here: Claude Code@"; the register form was prefilled with this computer's ~/.ssh/id_ed25519 with id_rsa as 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.
  • Previews of the five surfaces were shown to the owner before the change and approved.
  • Fifth round, red before and green after: machines.list over a real config.json that 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 real Dispatcher with a scratch home and registry, the previous handler answered -32603 internal_error, this one [('box', [])]); local_keys order at both layers, plus test_the_default_identities_are_in_the_order_this_computer_s_ssh_offers_them against ssh -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-web vitest run 4119 passed (page gates 216 included), tsc, eslint on the feature, gen:check (247 methods), npm run build with both boot goldens; ui-tui gen:rpc/gen:i18n, lint:i18n -- --check; ty check on the changed modules; pytest tests/test_docs_site.py tests/test_living_docs.py 18 passed, mkdocs build --strict clean; 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 in tests/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

  • Security impact considered: machines.local_keys returns key paths on this computer, to this computer's own page, never key material; subagents.presets carries no credential; nothing new leaves the host. The connect form reaches the machine only through the existing subagents.add and its proof ping.
  • Backward compatibility considered: agents_here is a new required field on machines.list rows (the page maps it defensively); the two new methods are additive; an older page against this server ignores both.
  • Rollback path is clear for risky changes: revert the commit; no stored data changes shape.

Related Issues

N/A (follow-up to #983 and #1005, from the owner's walkthrough on 2026-10-19)

Handsome-wzw and others added 5 commits October 19, 2026 01:07
…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>
@Handsome-wzw
Handsome-wzw requested review from 0xKT and LivXue October 19, 2026 04:02
@Handsome-wzw

Copy link
Copy Markdown
Contributor Author

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 gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: 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 the raven_everos workspace package)
  • uv run pytest tests/test_ops_connection_add.py tests/test_rpc_machines.py tests/test_rpc_subagents.py -q - 254 passed
  • npm 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 passed
  • npm run type-check - passed
  • npm run gen:check - generated contract matches, 247 methods

Comment thread ui-web/src/features/extAgents/MachineCards.tsx Outdated
…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 gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: make 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 passed
  • npm 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 passed
  • npm run type-check - passed
  • npm run gen:check - generated contract matches, 247 methods
  • npm run lint - passed with four existing warnings in untouched files
  • git 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 gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

The 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 passed
  • npm 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 passed
  • npm run type-check - passed
  • npm run gen:check - generated contract matches, 247 methods
  • git 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>
@Handsome-wzw

Handsome-wzw commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

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 gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

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 passed
  • npm 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 passed
  • npm run type-check - passed
  • npm run gen:check - generated contract matches, 247 methods
  • git diff --check 80e3e5266263..HEAD - clean

Comment thread raven/rpc/methods/subagents.py Outdated
Handsome-wzw and others added 3 commits October 19, 2026 15:40
…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>
…b' into feat/machine_next_steps

# Conflicts:
#	raven/ops/connection_add.py
@0xKT

0xKT commented Oct 19, 2026

Copy link
Copy Markdown
Member

Merge conflict: @Handsome-wzw, this PR no longer merges onto main. git merge-tree --write-tree of head 6ea01494 onto the current tip 1b9cbf85 reports a conflict in: ui-web/src/rpc/generated.ts. Please merge or rebase onto the latest main and resolve it; the review stands on the code once it merges cleanly again.

… 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>
@gloryfromca

Copy link
Copy Markdown
Member

Blocking: rebase or merge the current GitHub main and resolve the generated RPC client conflict.

I independently reproduced the verifier's result against github/main at 1b9cbf85e538: git merge-tree --write-tree HEAD github/main exits 1 with a content conflict in ui-web/src/rpc/generated.ts. The unchanged head's focused checks remain green: backend RPC subagents/machines, 208 passed; ui-web extAgents, 195 passed. My prior code stance remains suggestions-only once the branch is mergeable.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

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

LivXue commented Oct 19, 2026

Copy link
Copy Markdown
Member

No blockers in the reviewed scope.

Scope: I reviewed only the raven/agent/** portion of this PR -- 1 of 42 changed files, raven/agent/tools/raven_config.py. The remaining 41 files were read as context but are not covered by this verdict; the observations below sit in them and are filed here because no line of theirs can carry an inline note. Four of the commits here are #1005's own (204e96cb9, 35b5c2532, 53aec661c, bf3828d81, up to its head bf3828d819f7); they are left to that PR's review.

What I examined: the one changed line in scope, the describe subagents hint in _machines_line() (raven/agent/tools/raven_config.py:331-332), against the rule it describes: subagents_add refuses an unnamed add of a preset the machine already has, naming the entry there, before any ping (raven/rpc/methods/subagents.py:515-517, :544-570), and lets a named add through as a deliberate second entry. The hint matches the rule; the tool's description and parameter schema are unchanged, and the hint shows only when a usable ssh machine is registered. The tools in scope that reach the ops code this PR changes pass no keep_config (raven/agent/tools/connection_add.py:120, :156; machine exec through runner_from), so the ssh argv they send is unchanged.

The four points 0e978b3cf takes from my earlier draft, re-run at this head:

  • machines.list over a config.json that does not parse now lists the machine with no agents instead of failing (raven/rpc/methods/machines.py:147).
  • local_keys() offers the default keys in the order ssh -G prints them, id_rsa first (raven/ops/connection_add.py:103-109).
  • An agent row that already exists is no longer offered a machine (ui-web/src/features/extAgents/ExtAgentsPage.tsx:646), so its sheet no longer sends a toggle as "Connect on".
  • A preset whose add is still running is greyed in a form opened again (ui-web/src/features/extAgents/MachineCards.tsx:369-374); the moment that write lands is F1 below.

Observations outside the reviewed scope:

  • F1, Nonblocking -- a connect form that is open when its write lands offers the preset again, and pressing it is refused (ui-web/src/features/extAgents/store.ts:575-577, ui-web/src/features/extAgents/MachineCards.tsx:369-374). A form reads presets once, when it opens (store.ts:521-544). When the write lands, the in-flight mark is removed but nothing reads presets again, so a form opened during the write -- whose presets predate it -- shows the preset as pickable again, with no mark. Pressing Connect sends a second subagents.add; the new rule refuses it before any ping, so the form reports a refusal ("already on machine 'box' as 'codex@box'") just after a toast said the agent was connected. No ping and no write: a wrong label and one refused request. The PR's own case pins that state: ExtAgentsPage.test.tsx:3420 expects ['Codex', false] after the write lands, with a fixture whose presets() never reports the machine. One direction: when a write lands, read presets again if that machine's form is open, or count the landed <preset>@<machine> as taken, and move that assertion with it.
  • F2, Question -- did 0e978b3cf mean to move four ui-web files to the root prettier style? It reformats ui-web/src/features/extAgents/ExtAgentsPage.test.tsx, ExtAgentsPage.tsx, MachineCards.tsx and store.ts in full to prettier.config.mjs (no trailing commas, no parentheses around a lone arrow argument, 120 columns). ui-web has no prettier check -- pre-commit runs prettier for ui-tui and bridge only -- and 477 of the 557 ts/tsx files under ui-web/src (generated files aside) do not match that config, including source.ts, types.ts, store.test.ts and the fixtures this PR also changes. Of the 1,688 changed lines in those four files in 0e978b3cf, 121 remain once the previous version is formatted the same way; across the PR about 1,283 of their changed lines are format-only changes to code main already has, and the squash puts them all into one commit on main. If the reformat came from an editor or a hook rather than a decision for ui-web, could it come out, leaving the fixes?

Verification:

  • Probes on git archive trees of this head, of the previous head 80e3e5266263 and of the merge base, run outside the checkout with the main virtualenv (Python 3.12.13) and HOME / USERPROFILE / RAVEN_HOME in a scratch directory, each run printing raven.__file__; the page cases ran under vitest 4.1.11 in the archive's ui-web, with a cache directory of their own.
  • F1: a temporary case in ExtAgentsPage.test.tsx whose fixture reports on_machines: ['box'] once the first add lands. The form reopened during the write shows Codex greyed as being connected; after the write lands it shows ['Codex', false], presets() having been asked twice (once per opening); pressing Connect calls connectOnMachine a second time and the form shows the refusal, while the only toast says codex@box was connected; a form opened after the write lands shows the preset as already on the machine. The server half is this PR's test_add_on_a_machine_refuses_a_preset_the_machine_already_has_before_any_ping (tests/test_rpc_subagents.py:3467; its ping stand-in fails the case if called), which passes on this head's archive.
  • F2: prettier 3.8.3 from ui-web's node_modules with the root config. The four files at this head equal its output byte for byte; main's versions differ from it by 1,035, 179, 73 and 40 lines (in the order above), and formatting the versions at 681f9cc01 first leaves 78, 7, 23 and 13 lines of difference from this head. --list-different over ui-web/src: 477 of 557 files differ.
  • The earlier points: machines_list over three config.json files (valid, one with a // comment, one with a trailing comma) returns box with no agents each time; local_keys() with id_rsa and id_ed25519 pairs in a scratch home returns id_rsa first, the order ssh -G -F /dev/null prints on OpenSSH 8.9p1; the PR's cases for these and for the two page fixes (tests/test_rpc_machines.py:157, tests/test_ops_connection_add.py:342, ExtAgentsPage.test.tsx:3346, :3374) pass.
  • Tests on this head's archive: the five Python test files the PR touches, 310 passed and 1 failed -- test_install_node_closes_the_live_node_it_asked, which fails the same way on the merge base's archive, where a stand-in ssh starts a node that imports the main checkout's raven (gloryfromca reports 311 passed in a checkout); tests/test_raven_config_tool.py, 142 passed; vitest over ExtAgentsPage.test.tsx, store.test.ts, AgentsBody.test.tsx, state/hub.test.ts and components/HubHead.test.tsx, 205 passed. CI at this head has no failing check.

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 ui-web/src/rpc/generated.ts (b66cb8fb7) and join local_keys with #1005's new probe signature (681f9cc01); the conflicts 0xKT and gloryfromca reported are resolved.

Handsome-wzw and others added 2 commits October 19, 2026 17:54
…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>
Comment thread raven/ops/connection_add.py
LivXue and others added 2 commits October 19, 2026 21:38
… 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 gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

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.

Comment thread raven/config/held_secrets.py Outdated
…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 gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

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

LivXue and others added 5 commits October 19, 2026 15:32
…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.
@LivXue

LivXue commented Oct 19, 2026

Copy link
Copy Markdown
Member

@Handsome-wzw, three follow-ups on top of a75bcbf3, pushed as maintainer edits:

  • 68ea0118 fix(*): the register form's no-key note describes the kept password, not a key Raven makes. gui.machine.keys_none still said "pick Password: Raven then makes a key" (and its zh twin likewise), the mode feat(*): a machines hub tab, and registering a machine with a password Raven keeps #1005 replaced; it now says Raven keeps the password on this computer and logs in with it. The local_keys() docstring ended on the same claim.
  • e5884ddf fix(ui-web): keep a landed preset greyed until the connect form has read its presets again. In 91952bae the in-flight mark comes off before load(true) and the presets read that follows it, so for that stretch the reopened form offers the preset as free; and the reopened form's own presets read can answer last and offer it again. The mark now comes off once the presets are back in, and only the latest presets read is written. Two page cases: each fails on a75bcbf3, and undoing either half of the change fails exactly its case; your case passes unchanged. The note above the re-read drops its date.
  • dac563aa docs(ui-web): the connect form's state notes say when its presets are read again. The note on presets still said they are read when the form opens, and the one on mconnectPending that the mark means the write is still running.

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 store.ts and ExtAgentsPage.test.tsx, which I wrote in the root config's style to match the files as they stand.

Verified at dac563aa: vitest run src/features/extAgents src/state/hub.test.ts src/components/HubHead.test.tsx 268 passed and vitest run scripts/gates 216 passed; tsc, eslint, gen:check (247 methods), lint:i18n; tests/test_ops_connection_add.py, tests/test_rpc_machines.py and the two i18n test files 107 passed; the source-language, large-file and commit-message checks are clean.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

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>
@Handsome-wzw

Copy link
Copy Markdown
Contributor Author

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 gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

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

@LivXue

LivXue commented Oct 19, 2026

Copy link
Copy Markdown
Member

No Windows compatibility issue found in the inspected scope.

Reviewed head 1834c68ee3653666d5702a9c9d2cc0fd58c25a8c, base 1b9cbf85e538f461a2597d90b36644c29d2fdf3d. Inspected the complete 61-file diff (stacked on #1005) covering machines.local_keys, machines.list (agents_here), subagents.presets, duplicate-preset-on-machine refusal in subagents.add, configless password scrubbing in held_secrets, and the corresponding ui-web / ui-tui / OpenRPC updates. Exact-head archive blobs and reverse patch applicability were verified.

Windows compatibility notes:

  • connection_add.local_keys() expands ~/.ssh via os.path.expanduser, handles missing directories via OSError, and returns normalized ~/.ssh/<name> strings matching ssh -G output on Windows OpenSSH.
  • test_the_default_identities_are_in_the_order_this_computer_s_ssh_offers_them passes against native Windows OpenSSH (ssh -G -F /dev/null nobody@localhost).
  • held_secrets.held_secrets() treats a missing config.json as an empty config-credential set and still scrubs <RAVEN_HOME>/keys/*.password on Windows.
  • machines.list catches both ValidationError and ConfigReadError when reading agents_here.

Verification and limitations:

  • Native Windows 11 / Python 3.13.12 (uv run pytest ... -n 0 -x): 753 passed, 39 skipped across the touched transport, connection registry, local_keys, RPC machines, RPC subagents, CLI, ACP remote, node client, held-secrets, Oncall, and OpenRPC schema suites.
  • Two pre-existing POSIX-permission test assertions in untouched tests were deselected; 39 skips are explicit POSIX-mode or /bin/sh stand-in skips. No live browser UI run, real remote SSH node connection, or full-repository test suite was executed in this scheduled check.
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'

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

This revision 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/main is 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.

@LivXue

LivXue commented Oct 20, 2026

Copy link
Copy Markdown
Member

No Windows compatibility issue found in the inspected scope.

Reviewed head 166939fd22349c2c2ee1f70caff1c7866c90392f, base 55c145ceb87a11e06226add05da5372a9cf6f991. Rechecked the complete 61-file comparison against the previous review at 1834c68ee3653666d5702a9c9d2cc0fd58c25a8c: this revision merges current main (55c145ceb87a), with 53 patches identical and the 8 re-anchored patches (i18n/messages.json, raven/node/client.py, raven/rpc/models.py, rpc-schema/openrpc.json, tests/test_node_client.py, ui-tui/src/i18n/messages.generated.ts, ui-tui/src/rpc/generated.ts, and ui-web/src/rpc/generated.ts) inspected in full. Exact-head archive blobs and reverse patch applicability were verified.

Windows compatibility notes:

  • connection_add.local_keys(), machines.list (agents_here), subagents.presets, subagents.add duplicate-preset-on-machine refusal, and configless password scrubbing in held_secrets are unchanged and behave identically on native Windows.
  • raven/node/client.py cleanly integrates main's authenticated loopback MCP endpoint changes with the branch's remote.ssh_environment(target) askpass environment wiring.

Verification and limitations:

  • Native Windows 11 / Python 3.13.12 (uv run pytest ... -n 0 -x): 755 passed, 39 skipped across the touched transport, connection registry, local_keys, RPC machines, RPC subagents, CLI, ACP remote, node client, held-secrets, Oncall, and OpenRPC schema suites.
  • Two pre-existing POSIX-permission test assertions in untouched tests were deselected; 39 skips are explicit POSIX-mode or /bin/sh stand-in skips. No live browser UI run, real remote SSH node connection, or full-repository test suite was executed in this scheduled check.
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'

@LivXue

LivXue commented Oct 20, 2026

Copy link
Copy Markdown
Member

@Handsome-wzw, #1005 landed on main as 7376de68 and left this branch CONFLICTING, so I merged main into it as a maintainer edit: 85a3d0d3, on top of 166939fd.

Verified at 85a3d0d3: 19 Python test files (machines, connection add, subagents, the RPC schema and registration, raven_config, held secrets, transport, the ops CLI, node client, ACP remote, connections, the three on-call flow files, docs, i18n) 1302 passed, 6 skipped (shells this computer lacks); full suite 33765 passed, 3 failed, the same three failing the same way on main at 7376de68 (an npm on this computer's PATH, LibreOffice, a script root can read anyway); ui-web vitest run src/features/extAgents src/state/hub.test.ts src/components/HubHead.test.tsx 268 passed and vitest run scripts/gates 218 passed, tsc, eslint, gen:check (248 methods); ui-tui lint:rpc, lint:i18n and the gateway-types drift test; ruff, ty, and the source-language, large-file and commit-message checks are clean.

@0xKT: with #1005 in, the diff against main is this PR's own change.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

This revision 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/main is 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.

@LivXue

LivXue commented Oct 20, 2026

Copy link
Copy Markdown
Member

No Windows compatibility issue found in the inspected scope.

Reviewed head 85a3d0d320d1fa4304c4cffbd9409fecad950289, base 7376de6828cca99668ac6199dc32940a8517237a. Rechecked the complete 31-file diff after merging main (7376de68, which landed #1005) into the previous reviewed head 166939fd22349c2c2ee1f70caff1c7866c90392f: 23 changed files have identical blobs to 166939fd2234, and the 8 merged files (CHANGELOG.md, CONTEXT.md, docs-site/docs/machines.md, docs-site/docs/machines.zh.md, raven/ops/connection_add.py, raven/rpc/methods/machines.py, tests/test_ops_connection_add.py, and tests/test_rpc_machines.py) were inspected in full. Exact-head archive blobs and reverse patch applicability were verified.

Windows compatibility notes:

  • connection_add.local_keys(), machines.list (agents_here), subagents.presets, subagents.add duplicate-preset-on-machine refusal, and configless password scrubbing in held_secrets are unchanged and behave identically on native Windows.
  • raven/ops/connection_add.py cleanly merges DEFAULT_IDENTITIES and local_keys() with main's updated probe(..., password=...) signature and explicit Windows password-machine refusal.

Verification and limitations:

  • Native Windows 11 / Python 3.13.12 (uv run pytest ... -n 0 -x): 766 passed, 49 skipped across the touched transport, connection registry, local_keys, RPC machines, RPC subagents, CLI, ACP remote, node client, held-secrets, Oncall, and OpenRPC schema suites.
  • Two pre-existing POSIX-permission test assertions in untouched tests were deselected; 49 skips are explicit POSIX-mode, Windows password-refusal, or /bin/sh stand-in skips. No live browser UI run, real remote SSH node connection, or full-repository test suite was executed in this scheduled check.
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'

@0xKT

0xKT commented Oct 20, 2026

Copy link
Copy Markdown
Member

Not a blocker (review of 85a3d0d3, merged onto 7376de68): three things worth a follow-up. I reproduced the first and read the other two in the code.

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

  1. A config whose subagents value is not an object now breaks the Machines tab.

    • _row_out asks _agents_here for every row (raven/rpc/methods/machines.py:135). _agents_here catches ValidationError and ConfigReadError only (:156). With subagents a list or a string, update_subagents._raw_entries raises AttributeError.
    • Through the real dispatcher, machines.list answers internal_error for both shapes. On 7376de68 the same configs list the machine.
    • machines.add builds its answer through the same _row_out, after it has written the row and the ssh alias (:308-331). So with such a config an add that lands answers internal_error with the row already written, and a retry is refused as a duplicate. This follows from that order in the code; I ran only the list call.
    • CONTEXT.md's new line says such a row shows no agents. Catching the error there, or validating the section's type in _raw_entries, would make that true.
  2. One preset per machine is checked only before the ping.

    • _refuse_a_second_entry_there runs on the list read before the ping (raven/rpc/methods/subagents.py:515-517). After the ping the list is read again (:535), but the check is not run on it.
    • So a named add of the same preset on the same machine that lands during the ping is not seen, and the default-named add lands beside it: two rows of one agent on one machine, which is what the check exists to stop.
  3. "No key path" now has an exception the contract does not mention.

    • The module docstring and CONTEXT.md say nothing in machines.* carries an address, an account or a key path (machines.py:7, CONTEXT.md:451).
    • The new machines.local_keys returns this computer's ~/.ssh key paths to the page (:478-486), and the register form prefills one.
    • They are this computer's own file names, not anything about a registered machine, so the intent looks sound. The two sentences should say which paths are excluded, so the next reader does not take either side as a bug.

@0xKT
0xKT merged commit 2e390ad into EverMind-AI:main Oct 20, 2026
43 of 44 checks passed
@Handsome-wzw
Handsome-wzw deleted the feat/machine_next_steps branch October 20, 2026 06:28
0xKT pushed a commit that referenced this pull request Oct 20, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants