Repository navigation
fix(web): require an available machine for SSH terminals - #343
Open
theogravity wants to merge 170 commits into
Open
theogravity wants to merge 170 commits into
theogravity wants to merge 170 commits into
Conversation
This was referenced Oct 9, 2026
theogravity
force-pushed
the
fix/ssh-launch-availability
branch
from
October 11, 2026 08:48
0bb34ac to
6e4718e
Compare
theogravity
force-pushed
the
fix/segmented-selection
branch
from
October 11, 2026 08:48
23be566 to
4b50f6e
Compare
Captures the new line of work: a plain SSH terminal pane to any sshd-only host as the primary product, with two connection modes (stock ProxyJump "Jump", and a plane-blind sealed agent relay "Relay" that authenticates the connecting machine using a key that never leaves its home). The agent-destination boot from the #330 approach becomes an optional upgrade on top. Phased M1 (terminal + jump, no relay) / M2 (the sealed relay, own spec). Design doc only; no implementation. Supersedes the approach on feat/ssh-support (PR #330). Co-Authored-By: Claude <noreply@anthropic.com>
Rewrites the UI section around the real job (pick a destination; origin/key source only as a progressive follow-up; the agent upgrade is a secondary act in the pane). Records that reuse from #330 is code/ideas only and its connect UX is a cautionary example, not a base. Co-Authored-By: Claude <noreply@anthropic.com>
…nabled, fail-closed, audited) Co-Authored-By: Claude <noreply@anthropic.com>
…mitives); spec supersedes SSH-SUPPORT.md Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…laim, machine-identity gap, grant/pin phasing, SSH_AUTH_SOCK + sharing, gate semantics Co-Authored-By: Claude <noreply@anthropic.com>
…not origin (add B's signature), identity filter from snapshot, saved-hosts store + view-only/direct tests, host-key + mode-naming + success fixes Co-Authored-By: Claude <noreply@anthropic.com>
…(spec 4.3 + plan nodeCanSsh mirrors nodeCanManageFor, no granted param), relay needs a per-machine ES256 signing key, signatures run both ways with the nonce bound, plane-off-transport-path precision, quota/quota-issuer and pin-phrasing tidy-ups
…rs node:<id> (the M2 gap is the node end), plan runs wholly in the worktree, grammar port carries the engines' real import set (errors/results/fixtures included, tests ported not hand-written), kysely three-arg addColumn + 0031-pattern migration test, one ssh-enabled service as the single change site, protocol bump 15->16, audit name node.ssh_enabled.update + docs list, relay pins always strict
…sess-principal peers.json (machine relay gets a separate always-strict store), grammar port gains ssh-frames (the fixtures' type imports, verify-types-visible), Task 2 edges fixed (login-path kept, expandTilde barrel-internal), predicate JSDoc stops promising codes, worktree cd on all commits, plane-side reconcile test added
…s commit, node-side mirror write carries the whole {on, changedAt} wire like writeMaintenance
…efusal codes into subshell-protocol
…enabled route
setNodeSshEnabled is the one change-site (row + audit, Task 7 layers the
push); the route is gate-only. Owner-only management (canManage, admin on
local), audit metadata is {on} alone. Node view carries sshEnabled on both
list and detail.
…ready reporting (protocol 16) Completes the loop Task 6 opened: the plane is the sole author of nodes.ssh_enabled, and this is the machinery that reaches the machine. - node: ssh-enabled.ts mirror (0600 temp+rename like maintenance, but fail-CLOSED the other way: absent and unreadable both refuse, only a parsed on:true allows); set_ssh_enabled executor writes it verbatim and memoizes as reported; the ssh_enabled event carries a mirror that moved mid-session (corruption, chiefly); ready.sshEnabled rides the same synchronous seed + optional-spread as maintenance. - protocol 15 -> 16: NodeSshEnabledWire/parseNodeSshEnabled, the set_ssh_enabled arm, the ssh_enabled event, ready.sshEnabled (drop-not-fatal). Breaking on purpose: pre-16 agents are wire-indistinguishable from a 16 agent whose mirror says no, and a capability gate cannot live with that ambiguity (spec 4.3). - plane: pushSetSshEnabled (local is a no-op - the row is its mirror) wired into setNodeSshEnabled as the one-function write+audit+push; decideSshEnabled + reconcileSshEnabled never adopt - every disagreement, and every row-ON-with-silent-node, ends with the ROW pushed down, stamped verbatim, and no audit row (pushes are not acts). Tests: node 866/866 (new 17), plane services/nodes 694/694 (new 14 + 4 hook-door cases), protocol 364/364 (version pin + ssh arm cases).
…ld the older-snapshot fixture on a valid ledger prefix (the tip-removal fixture hardcoded 0046 as newest; adding a migration must move both the ceiling and the fixture's strip set)
…-16 hold consequence, ported wiring claims say 'until Plan 2' (barrel, parseNodeCommandBody arms, node-results validators, the retired command quad), Gate A values attributed to their actual freeze, spec pointers trimmed to honest anchors, route comment + import fold + test-restore audit note
…er re-tagged to the Gate A lineage, and the Match-exec disclosure pointer moved off the wire JSDoc (which does not carry it) onto the grammar header that does
…now states the shipped order (walk refuses a seen exec before -G ever runs; the disclosure covers what the bounded walk could not see)
…esolve, rendered ssh panes, gated surface, owner-only input)
…launch block, result validators join node-results
… terminal auth left to the server
…t-pin door The caller's host-key pins as destination plus SHA256: fingerprint (the key line itself is display-excluded by construction), the delete that re-opens the TOFU decision, and the §9 explicit-pin dialog for a destination trusted under an SSH HostKeyAlias where the capture cannot find the row. The add form runs the substrate; a conflicting pin is the server's RED line naming the recovery. Co-Authored-By: Claude <noreply@anthropic.com>
…e-pair The node page mounts SshTrustSection in place of the bare card: the shared NodeSshTrustCard keeps the display, and the section adds the two plane-owned halves the shared card must not carry. The §4.6 honesty line marks this rendering for DISPLAY only (the check happens on the two machines' own dashboards), and the §4.5 re-pair act renders inside each pinned peer's row through the card's new optional renderPeerAction slot, owner-only. The plane's re-pair route (the node.ssh_machine_pin.repair act) does not exist yet, so the button is DRAWN DISABLED with its reason rather than wired to an invented endpoint (T15 report flags the gap). Co-Authored-By: Claude <noreply@anthropic.com>
/settings/ssh composes the queue (first: it is the section whose inaction has a deadline), the standing grants, and destination trust. It rides the PERSONAL Settings group, not the admin one: every route the page reads is the caller's own ledger. Fingerprint for the icon, uniqueness still pinned; General keeps lighting exact-match, so the /settings/ path never illuminates the admin group. Co-Authored-By: Claude <noreply@anthropic.com>
… expiry Task 15 review MINORs: the §8 name/selector edit over PATCH /api/ssh/grants/:id (static-titled dialog, fingerprints can never ride the body); a pre-selection the roster no longer carries renders disabled and named and never reaches the approve POST (this screen is the membership fence the server does not run); the 24 h deadline localizes like the sibling house queue. Cap sentence aligned with the server's verbatim.
The sanctioned recovery when a peer's machine key genuinely rotated: the
owner of A re-delivers the peer's CURRENT registered public pair and A
replaces that one peer's stored pin. No trust-anywhere exists, and normal
byte-strict pairing is untouched - an UN-repaired peer's block persists.
Protocol: the ssh_machine_pin_repair arm carries peer id + the two peer
JWKs in the relay-open's own carriage (shared validators now, private d
refused at the grammar); result ack {repaired, peerNodeId}; census 9->10.
Node: MachinePinStore.repair (the only replacement path; pin delegates to
it), the command handler deep-validates both halves (bytesOfJwk) BEFORE
any write and is deliberately UNGATED (ssh_register_identity doctrine -
recovery must reach a blocked pairing); acks ids only, no audit here.
Plane: POST /api/nodes/:id/machine-pins/:peerNodeId/repair, cookie-only,
EXACT owner (route + service both check ownerUserId; admin included not),
agent machines only, A online, peer's registered pair read from the
identities store; audits node.ssh_machine_pin.repair naming the two node
ids only, and audits NOTHING on any refusal; docs/security.md §10 entry.
SPA: the T15 disabled placeholder becomes a live owner-only Re-pair per
peer row (gate = access owner, the route's own test), refusal renders on
the button that failed, success note + refetch.
The grants screen could list, edit and revoke (T15) but not create: the only roster read was request-scoped, and a manual create has no pending request. Adds the roster-by-node read (same T11 ssh_agent_identities command, no new protocol arm), gated through the key home's own gate before the machine is asked: foreign/vanished 404, local refused at the same door as the create, offline named-fail-closed, no fabricated empty roster, no audit row. The SPA create picker reads A's live roster, caps the selection at SSH_MAX_GRANT_FINGERPRINTS as a red hard error, and POSTs the existing sshCreateGrant contract, so the row is identical to an approved first use's.
…ver the missing-node 400
…agent Spec 23 drives the whole milestone-2 path against this host's REAL ssh-agent, a REAL sshd, real node daemons, and the real broker: B holds no key and authenticates through the pane's proxy socket; a plane-side capture (broker routing seam via the sanctioned test swap) proves only opaque JWE envelopes cross; the OpenSSH 10.x numbering (identities 11 / answer 12 / sign 13 / sign-response 14) is asserted end to end with an independent wire reader; grant revoke between handshake and D cuts signing at the never-answers hang listener; SetEnv sandbox makes "Set up Subshell here" a real install that never prints the setup key in the pane; a rotated D host key is a hard block until delete-plus- fresh-capture TOFU. Failures name their stage; every stage boundary prints elapsed seconds so the 30 s lifetime cap cannot hide a skipped leg. Fixture extends startSshFixture with SetEnv lines (one combined line: repeated SetEnv keywords do not accumulate), a held-dead-port ladder, and the ssh-agent/ssh-add gate; the relay fixture adds machine A's pre-pinned known_hosts home, B's key-less config home, and rotate. Co-Authored-By: Claude <noreply@anthropic.com>
Protocol 18 with migrations 0049-0052; server before nodes; lists every package the branch touched. Co-Authored-By: Claude <noreply@anthropic.com>
…urposed to open-ACK); fix child-exit comment side (B pane ssh child)
…ne (PR review Important 1)
…versized answer PR #338 round 2: pin() now throws when the peer already has an entry (repair stays the only replace route); the agent roster read caps at SSH_ROSTER_MAX_IDENTITIES, refused by name never truncated, mirrored at the wire validator; the mobile push kind union gains maintenance.
…r and posture notes PR #338 round 2 comment wave: the push category names replace the stale count, handshake-grace now names both broker spellings, the base64url fingerprint spelling is documented as internal, the IPv6-literal selector limitation is written down (code and spec), and the blind broker's A-side invisibility is recorded as accepted posture.
…l match); pin it in the matcher suite
…n lives at the manual door only
…ker reads a bare hash as a hex color)
theogravity
force-pushed
the
fix/segmented-selection
branch
from
October 12, 2026 00:36
4b50f6e to
8627e41
Compare
theogravity
force-pushed
the
fix/ssh-launch-availability
branch
from
October 12, 2026 00:37
6e4718e to
f0ab89b
Compare
theogravity
force-pushed
the
fix/segmented-selection
branch
from
October 12, 2026 09:33
8627e41 to
1675aa7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When no machine can run an SSH terminal, New subshell replaces the destination form with setup guidance and buttons to open machine settings or add a node. Enrollment guidance respects the user's permissions.
Availability includes ownership, launch permission, online status, maintenance, protocol hold, and SSH enablement. The form polls while open and blocks a stale selection without switching machines; failed reads offer Retry.
Stacked on #342. Validation: full test suite, regression coverage for unavailable/stale machines, load retries and action navigation; type checks, lint, prose checks and SPA build. Code review has no major or minor findings.