cli-node-v1.0.0
Major Changes
- #184
711b5faThanks @theogravity! - Marked 1.0.0. The control plane, the node daemon, and both desktop apps
all mark their first stable release. This entry changes no behavior; it
records the milestone, and the entries below it are what the milestone is
made of.
Minor Changes
-
#177
f77b8a0Thanks @theogravity! - The node ↔ control-plane link is now encrypted end to end. Every/ws/nodeconnection negotiates a fresh key with libsodium'scrypto_kx(each side authenticates against the long-term identity it pinned at pairing), and after that the socket carries only ratchetedcrypto_secretstreamciphertext, never resynced. A server on plainhttp://no longer puts launch commands, pane bytes, or bearer tokens on the network in the clear; whoever can see the node's network can no longer read it.This is a hard protocol cutover (13 → 14, minimum node version 0.17.0) with no plaintext fallback. Deploy order: server first, then nodes. Nodes enrolled before the link existed pair themselves on their first connect after updating (the agent mints its keypair and registers it over the socket its bearer key already authenticates), and rotating a node's key re-provisions the link identity through the same self-heal. A node that skips or fails the handshake is refused with close code 4410 and retries with its backoff, never silently degraded to plaintext.
-
#170
ac9ec22Thanks @theogravity! - Nodes now age out their own pane logs. A subshell's transcript (every byte the terminal rendered, typed secrets and pasted tokens included) lives in a log file on the machine that ran the pane, and until now the only node-side deletion was the control plane commanding it at delete time: a node that was offline for the delete kept the transcript indefinitely, and a terminated-but-kept subshell kept it for the life of the machine. The agent sweeps its own disk instead (one pass at boot, an hourly pass after it, with no dependence on the plane being reachable), deleting the log of any subshell whose pane its tmux census does not find live, older than a window configured on the node itself:SUBSHELL_LOG_RETENTION_DAYS/SUBSHELL_LOG_RETENTION_HOURS(the environment wins), else the matchinglogRetentionDays/logRetentionHoursfields ofconfig.json, else a default of one day, deliberately shorter than the server's 30, because a node is not where transcripts should accrete.0days and0hours together keep everything forever. A running pane's log is never swept (it is the live replay buffer), a liveness probe that cannot answer counts the pane running, and only pane-log name shapes inside the data dir (never a symlink) are ever touched. This is a tightening on upgrade: an unconfigured node deletes non-running transcripts older than a day. The window now has a setter on the machine itself: the node's loopback dashboard shows the effective days and hours with the layer each came from (environment,config.json, or the default) and writes changes toconfig.json, which the next hourly sweep picks up without a restart. A field the environment forces is read-only there, named by its variable, the same rule the debug-logging switch follows; the plane has no counterpart route, because the policy belongs to the machine whose disk it ages. The boot sweep is started before the agent's first connection to the plane but no longer waits for it, and the liveness census probes concurrently, so a wedged tmux can slow a sweep but can no longer stall a node's connection. Every refusal at that setter, a validation 400 as much as an env-forcing 409, writes a line to the agent's own log, because there is no audit row on this surface and the log IS the machine's record of its own decisions.
Patch Changes
-
#170
ac9ec22Thanks @theogravity! - An update started from the node's own loopback dashboard can no longer be refused by a stale fact. During the daemon's boot window the page re-proves supervision with one live service-manager query and now carries that answer into the executor, instead of the executor re-reading the same boot-time report, still null, and answering "not supervised" to a node the page had just proved supervised. A caller with no proof, which is every plane-commanded update, keeps the old refusal exactly.The force option now states its own limit: a downgrade the control plane will not accept leaves the node held offline for about ten minutes and is then reversed automatically, so the card says that beside the checkbox instead of letting the success line imply a durable downgrade.
The same threading now covers the service card. With no daemon in the process (the standalone dashboard), the service route decided pane safety from a fresh manager read but used to word its refusal from the daemon report the route had just fallen back past, so a machine whose fresh read answeredunknown("nobody could read the definition") got the CERTAIN sentence ("would close every subshell"). The resolved report'spaneSafetynow rides to the wording, exactly as the update route's proof already does; akillsanswer keeps the certain sentence, and aliveRuntimetest seam pins both halves. -
#180
b28726aThanks @theogravity! - Dialog and tab-strip polish, and the provider admin's sharper edges. Dialog actions stay in one right-aligned row and dialog headers stay left, always: shadcn's viewport breakpoints (stack under 640px, center until 640px) tested the WINDOW, so page zoom or a narrow shell re-stacked buttons and centered text inside a comfortably wide dialog. Page tab strips (Users, Logs, Nodes) are content-sized now instead of stretching two labels across the page; the equal-share switch stays for in-row controls. The copy icon shrank to sit inside value rows. On Settings → Auth: the provider form shows its slug id live under the name, the remove confirmation lists its effects and names the exact row (slug id and issuer) and notes that re-adding the same slug id restores the accounts, close-capable toggles are DISABLED with a tooltip when a provider is the last open one (the 409 remains the enforcement, it just stops being the introduction). Thecli-nodebump covers only the smaller copy icon, which the node dashboard shares. -
#173
766d4a7Thanks @theogravity! - Notifications get quieter. A Stop hook no longer rings "Done, waiting for you" while the session is parked on background work; approval pushes fire only for the notification types that genuinely need a human; a pane pushes at most once until its owner opens it, escalation excepted; and the sidebar dot becomes a bell for exactly as long as a push sits unanswered. -
#170
ac9ec22Thanks @theogravity! - Every name is normalized the same way, and the attach journal line can no longer be forged. Manual subshell renames, named creates and both workspace name doors now run the human-typed name through the sharednormalizeLabelinstead of a bare trim; a hand-typed name no longer carries escape bytes into the restart journal line or another user's sidebar, and a name that is nothing but invisible characters is refused (subshells, workspaces) or becomes an unnamed create (a create never 400ed on a blank name and still does not).normalizeLabelitself hardened: NFC first (so two spellings of one name compose to one string and the cap counts characters of the canonical form), Unicode format characters dropped (bidi overrides, zero-width joiners and spaces, soft hyphen, BOM, emoji tag characters: the invisible half of what CR/LF do to a log line), the emoji presentation selectors dropped, and unpaired surrogates dropped, which nothing downstream could render or compare anyway. Node names and device labels inherit all of it (the node patch is that inheritance: a name typed on the machine is now stored the same rule the plane applies). Separately, the per-attachws attachjournal line clamps its User-Agent to the characters real UAs actually contain BEFORE slicing to 90 (a client can no longer close the quoted field, break the line, or mint a second record in the forensics the operator greps), and a disabled account's session cookie is refused on the no-token WS attach path, the sameaccountDisabledcheck every REST surface already applies. -
#169
78fd431Thanks @theogravity! - Fix the fresh-terminal replay so typing lands on the visible prompt.The attach replay ended with the client's cursor at the bottom of the grid
while the pane's cursor sat at the prompt near the top, so every live byte
(echo included) painted below the visible prompt (the "prompt at the top,
typing off-screen" report). The replay now ends with an absolute move to the
pane's real cursor: a newpane_cursornode command (protocol 12 → 13) feeds
it, and an older agent that cannot answer simply gets the previous behavior.
A booting pane's first frame now waits for the shell's first paint instead of
shipping the blank grid, the log file is waited for (the whole-grid poll
fallback stays for panes that never get one), and bytes already queued at a
booting viewer are dropped rather than replayed on top of the capture, which
double-painted prompt sequences into ghost prompts. -
#170
ac9ec22Thanks @theogravity! - The node's update paths match the server's hardening and two dashboard truths become honest.subshell update --rollbackand the loopback dashboard refuse to install a<binary>.previousthat cannot run (it is probed withversionbefore the rename, and the automatic 4406 revert records the failure instead of swapping in an unbootable copy); the rollback copy is now made atomically, so an interrupted copy can never leave a truncated one. The probe carries the server twin's bounds as well (stdin ignored, stdout drained, a 10-second hard timeout), so a rollback copy that never exits is refused as unable to run instead of hanging the boot before the exit its service manager is waiting for. Live writes toconfig.json(the dashboard's retention card and debug-logging switch, andsubshell configure) now go through one fresh re-read merge, so concurrent writers can no longer revert each other's fields on the file that also holds the node key. And the node's own Settings page reports whether its hourly retention sweep is actually scheduled, so its copy promises the next sweep only when one will run and says "at the next restart" when the daemon armed no timer. -
#170
ac9ec22Thanks @theogravity! - The node's pane-log retention sweep can no longer delete the transcript of a subshell that restarted while the sweep was running. The pass censused tmux once at the top and then walked the files; a restart landing in that window reuses the same log path append-only with the old mtime intact, and with the default window of ONE DAY almost any aged file is a candidate, so the sweep unlinked a live transcript out from under the capture child, and the pane kept writing the dead inode until a relaunch. Every eligible file is now re-probed with a freshhasSubshellimmediately before its own unlink; a probe that cannot answer counts the pane running, the census's own unknown-is-not-dead rule. An orphan log (no record to probe) still ages out: that is what a missed delete leaves behind. -
#170
ac9ec22Thanks @theogravity! - A service definition nobody could read is now refused with the honest sentence. The destructive verbs (nodestop/uninstall/restart, nodeupdate, the server's own restart and update) already fail closed when the supervision definition reportsunknownor nothing at all: one refusal code, because an unreadable definition is not evidence of safety. But the WORDING promised, with certainty, that every pane would die. Only a definition that actually answeredkillsearns that sentence now;unknownand no-report-at-all say the definition could not be read, on the plane's node surfaces, the node's own loopback dashboard, and the server's own restart and update routes alike. -
#176
db5951aThanks @theogravity! - "Waiting for you" now clears on agent nodes. The only alive-path clearer was the plane's idle watcher, which can only observe a log on the plane's own disk, so a pane running on a node stayed amber from its last Stop or approval until the process died, however hard it worked. Claude Code's hooks now report a third attention kind,resumed(prompt submitted, or a tool starting after an approval), and the pane's own report clears the stamp from wherever it runs; it never reads the hook payload and never rings. Rollout: update the nodes FIRST, then the Server. The Server ships the new hook, and an oldersubshellbinary rejectsresumedas an unknown argument with exit 2, which Claude Code reads as a blocking error on every prompt and tool, so a node left behind stalls its panes until it updates. An updated node against an older Server is harmless: the report is a silent no-op there. -
#170
ac9ec22Thanks @theogravity! - Two round-3 audit fixes to how a node updates its own binary. Theupdateexecutor (for the plane-commanded path and the node's loopback dashboard alike) now refuses a command that names a version not NEWER than the running agent, so the downgrade refusal is no longer only the plane's: a confused or compromised control plane cannot walk an agent backwards by accident.forceanswers the refusal (the loopback dashboard offers an explicit "allow a downgrade" through the same executor, which is the keyboard act a well-behaved plane never pairs with force), and an equal version is refused even under force. And the swap itself (the node's own port of the server's shape) no longer moves the live path:<binary>.previousis made a second name for the running agent (a hardlink, or a flushed copy where links are refused) and ONErenamelands the new bytes over the running image, which is measured safe. The old two-rename order had a window where neither name held a bootable agent, the boot-revert living inside the missing binary, so a kill or power loss there meant a hand at a keyboard on a deliberately headless machine; now every crash state leaves a bootable binary at the unit's path, andsubshell update --rollbackreads the same.previousit always did. -
#182
6955725Thanks @theogravity! - The no-em-dash voice rule applied to shipped copy: every string a person reads on a screen or in a terminal now carries its breath with a comma, colon, parentheses, or a full stop. The tray update item reads "Update available: Subshell Server 0.8.0" (both apps), the node window title "Subshell Client: Node", network plugin hints, both CLIs' refusals and prompts, and the browser-rendered error messages lose their dashes, and so do the /docs endpoint descriptions, the shared MCP tool descriptions, and the desktop apps' permission prose. No wire name, error code, id, or log line changed. -
Updated dependencies [
711b5fa]:- @subshell-ai/plugin-api@3.0.0
- @internal/pane-runtime@1.0.0