Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
b5a0ed7
docs(spec): node credential hot-reload + Nodes status dot and filters
theogravity Oct 11, 2026
5d52047
docs(plan): implementation plan for credential hot-reload and Nodes f…
theogravity Oct 11, 2026
4d34a9b
docs(plan): require establishing the connection before config flips (…
theogravity Oct 11, 2026
6d1c6ff
feat(node): connection-identity compare and in-place field swap for c…
theogravity Oct 11, 2026
55dcbf4
fix(node): hot-reload connection credentials on the beat and before e…
theogravity Oct 11, 2026
3c07a1c
feat(web): pure node-filter lib for the Nodes page
theogravity Oct 11, 2026
b7b2549
feat(web): status dot replaces the invisible Nodes status chip
theogravity Oct 11, 2026
ca3a4a6
test(web): dot assertions replace the removed status chip in node-row…
theogravity Oct 11, 2026
b531be7
feat(web): Nodes search + status/access filters with a zero-match state
theogravity Oct 11, 2026
31a1286
docs(node): server-url copy says what the daemon now does — live on t…
theogravity Oct 11, 2026
c7f63e6
docs(web): server-url surface says the node applies within a beat
theogravity Oct 11, 2026
0a2de20
fix(node): the swap gate refuses an un-dialable target; own writes st…
theogravity Oct 11, 2026
814194d
fix(copy): use consistent, conversational language across Subshell (#…
theogravity Oct 11, 2026
ce390c1
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 11, 2026
fc5fcf9
chore: release package(s) (#361)
github-actions[bot] Oct 11, 2026
d07fab4
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 11, 2026
da13bbb
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 11, 2026
f8c4202
fix(web): simplify the nodes table and show owners (#363)
theogravity Oct 11, 2026
0d55da5
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 11, 2026
b670260
chore: release package(s) (#364)
github-actions[bot] Oct 11, 2026
9c78549
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 11, 2026
a4c2fc6
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 11, 2026
d4c38ad
fix(ci): regenerate the release index after competing pushes (#365)
theogravity Oct 11, 2026
0c545d3
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 11, 2026
118d9cf
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 11, 2026
9ffd530
fix(ui): explain allowed directories and who can edit them (#366)
theogravity Oct 11, 2026
6844d08
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 11, 2026
c05eb71
docs(website): align copy with the content guide (#362)
theogravity Oct 11, 2026
a1139ca
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 11, 2026
9b81267
chore: release package(s) (#367)
github-actions[bot] Oct 11, 2026
1ffaaf5
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 11, 2026
b2d7db7
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 12, 2026
ebc8ca5
fix(nodes): refresh service controls and guard updates during server …
theogravity Oct 12, 2026
c77604e
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 12, 2026
296c7d5
chore: release package(s) (#369)
github-actions[bot] Oct 12, 2026
9fa5fe2
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 12, 2026
591ab47
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 12, 2026
9352359
Explain disabled controls across the app (#370)
theogravity Oct 12, 2026
19f8f6f
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 12, 2026
f2efaee
chore: release package(s) (#371)
github-actions[bot] Oct 12, 2026
5aaec49
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 12, 2026
5b1c5f6
chore: refresh releases.json [skip ci]
github-actions[bot] Oct 12, 2026
da94fd4
Unify dashboard tables, layouts, and provider configuration
theogravity Oct 12, 2026
671ea53
Integrate node credential reload and filters with dashboard redesign
theogravity Oct 12, 2026
adc7244
Fix review findings in provider saves and live node configuration
theogravity Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 9 additions & 0 deletions .changeset/node-live-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@internal/node": patch
"@internal/desktop-client": patch
"@internal/server": patch
---

Reload updated node credentials and server addresses before dialing and on the heartbeat, preserving running subshells. Add search, status, and access filters to the Nodes table, and describe address changes without requiring a restart.

Reject WebSocket URL fragments and invalid credential headers before replacing the running connection, without logging credentials.
26 changes: 26 additions & 0 deletions .changeset/tables-in-cards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
"@internal/server": patch
"@internal/desktop-client": patch
---

Present node, sign-in provider, and desktop update tables in cards for consistent spacing and backgrounds. Use normal body-sized values in node tables. Show presets in tables grouped by agent with separate auto-restart and cross-agent use columns, preserving command reveal, copy, edit, clone, and delete actions.

Show saved prompts and prompt stacks in card-contained tables, retaining expansions and row actions. Make node names link to their details.

Click preset names to expand their commands, matching prompt rows; editing stays in the actions menu.

Keep list rows compact and adapt Nodes, Presets, and Prompts columns to the available card width, keeping names and actions visible on small screens.

Match the subshell table to the shared card, typography, compact rows, and responsive columns, and align selection checkboxes. Group destructive menu actions at the bottom behind a divider.

Show each subshell group in its own titled card, matching preset groups, with selection scoped to each group.

Use plain provider and role labels, adapt user and sign-in provider tables for phones, and present dashboard backup and restore workflows in settings cards.

Replace badge-styled field values throughout the dashboard with plain text, retaining warning emphasis and compact counts.

Move sign-in provider switches into a Configure dialog with explanations, retaining one-field updates and the last-provider safeguard. Show Yes/No values in the table.

Use consistent margins and fluid widths across dashboard lists, details, and server settings, including wide screens.

Apply provider save responses before enabling further changes, preserving dependent settings and last-provider safeguards during slow refreshes.
7 changes: 7 additions & 0 deletions .claude/rules/code-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,13 @@ as precedent for inline schemas elsewhere.

## JSDoc Comments

Follow [the repository's writing voice](../../AGENTS.md#writing-voice) in
comments and JSDoc. Explain the reason or constraint in natural, active English,
using contractions where they fit. Keep useful technical detail, but avoid
shouted emphasis, elaborate metaphors, and accounts of who requested a change.
Describe the behavior a maintainer needs to preserve. For example:
"Fall back to agent.log until the daemon writes its first log under the new name."

All public classes, methods, and functions should have JSDoc comments that describe:
- What the class/function does
- Parameters and their purpose
Expand Down
37 changes: 29 additions & 8 deletions .claude/rules/design-system.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
# Design System

The authoritative reference is [`docs/design-system.md`](../../docs/design-system.md);
`bun run lint:design` enforces it and fails the build on a violation.
`bun run lint:design` checks tokens and contrast; layout and interaction rules
also need review. Read [Dashboard pages and tables](../../docs/design-system.md#dashboard-pages-and-tables)
when changing page spacing, lists, row actions, or configuration dialogs.

**Pick a role, never a number.** Six type roles (`display`, `heading`,
App copy follows [the repository's writing voice](../../AGENTS.md#writing-voice):
write as you'd speak to a peer, with active verbs, natural contractions, and
varied sentence structure. Explain what's happening and what the reader can
do next. Keep explanations brief without squeezing them into long sentences.
Review tone by reading it aloud; the linter can't tell whether prose sounds
natural.

**Pick a role, never a number.** Five type roles (`display`, `heading`,
`label`, `body`, `detail`) at two weights, `strong` (600) and
`regular` (400):

Expand All @@ -19,17 +28,29 @@ Spacing on the 4px grid. A `text-[13px]`, a `font-size: 14.5px`, a
`fontSize: 17` or a hex outside a token file is refused by the check; if
the system lacks what you need, add the token to every surface in one change.

**Every UI explanation is at most two sentences, and UI copy uses no em dashes** (operator ruling 2026-09-21); surplus becomes docs or a disclosure, never help text. A lone em dash in a table or detail cell is an absence placeholder, not copy (the 2026-09-25 shipped-copy sweep kept the placeholders and rewrote only strings carrying words).
**Aim for one or two short sentences per UI explanation.** Use more when
consequences or limits need it. Keep warnings at the decision point; move
optional background to docs or a disclosure. UI copy uses no em dashes;
an absence placeholder in a table isn't prose.

**Agent** means the AI agent in a pane. Call the node background program the **daemon** or **node daemon**; a compact version column can say **Version**. See `apps/docs/STYLE.md` for examples.

Line items are `label` over `detail`, differing by weight and colour. **Every
explanation a control gives about itself is `detail`** (its help text, a
"set by the environment" note, a saved-vs-running line, a validation error),
all at one size, the same `detail` that carries metadata (timestamps,
versions, counts); `body` is running text not attached to a control.
**A dialog title is static: never interpolate user data into it unless a
spec says so** - long names wrap the heading and shove the content down;
name the act ("Send prompt to agent?") and let the body carry the data
(ruling 2026-09-30).
versions, counts). Table headings and values are the exception: they use
normal `body` size, with muted colour for secondary values. Outside those
tables, `body` is running text not attached to a control. A tooltip
popup is the floating-surface exception: it carries `body` (amendment 2026-10-12;
see the reference).
**Keep UI titles short and fixed.** Name the action ("Insert this prompt?",
"Close this subshell?") and put names, counts, versions, URLs, and other
variable details in the body. Apply this to dialogs, confirmations, cards,
and action headings. Object names can still appear in lists and detail
fields. Name-length limits and historical specs aren't exceptions. Follow
the [content guide](../../apps/docs/STYLE.md#product-copy-and-confirmations)
for action labels, consequences, and examples.
**A required input takes a gold `*` at rest and a gold caption once the caret
leaves it empty; a hard error (a refused value, a server refusal) stays
`text-destructive` red** (ruling 2026-09-30: gold says "nothing typed yet",
Expand Down
4 changes: 2 additions & 2 deletions .claude/rules/security-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,14 @@ Node mechanics depth (link close codes and pin lifecycle, §6; setup-key revocat
- **Maintenance is a routing preference, never a quarantine:** enrolled, answers everything, takes no new subshells; turning it ON **terminates every subshell running there**, whoever owns them; retroactive by design, and affected owners learn by push. Owner-only (admin on `local`): the re-share gate `canManage`, NOT `nodeCanConfigure` and NOT the wider `canRetire` gate delete/re-register sit on; the machine's own `subshell maintenance on|off` sets the same flag; newer stamp wins, ties to the plane, so the node's mirror check is defence in depth, not a second switch. Containment is deleting the node, rotating its key, or clearing shares, all cookie-gated. Audited.
- **Service control** (signed command; cookie only, owner or `edit`, `local` refused, audited `{verb, forced}`): the agent refuses unless the manager reports this very pid, and when the definition would take its panes down without `force`; **`stop`/`uninstall` are owner-only and structurally one-way** (they end the socket that would carry the undo); `force` is refused outright on `start`/`install`.
- **Repointing** (signed `set_server_url`) is **owner-only and a real widening**: the machine dials the named host with the node key, valid on THIS plane; loopback refused outright; addresses validated by component, stored canonicalized; audited naming the NEW value only; takes effect next start.
- **Disclosure surfaces:** the runtime report (supervision, paths, tmux) attaches only for an ONLINE agent node, viewer owner or `edit`, never `local`, never `view`. `ready` discloses host facts (agentVersion, os/arch, hostname, dataDir, capabilities, selfInvoke prefix, homeDir); `detect` answers ONLY the binaries/env names the plane asked (union of `subshell.hostEnv` across enabled manifests), never scans, so a plugin install (admin) decides what env names every node may be asked. `held` is disclosed to every viewer; `runtime` is not. A node view carries `ownerLabel` (whose machine) ONLY to a cookie-admin, the `?all=1` setup-key precedent (ruling 2026-10-10); every other viewer's payload lacks the field. Agent log over HTTP (owner/`edit`) never carries pane content or argv. `PUT /api/nodes/:id/logging` (an `edit` act, cookie only, `local` refused) is refused while `SUBSHELL_DEBUG_LOGGING` forces it on: a write the next read would mask is not a success.
- **Disclosure surfaces:** the runtime report (supervision, paths, tmux) attaches only for an ONLINE agent node, viewer owner or `edit`, never `local`, never `view`. `ready` discloses host facts (agentVersion, os/arch, hostname, dataDir, capabilities, selfInvoke prefix, homeDir); `detect` answers ONLY the binaries/env names the plane asked (union of `subshell.hostEnv` across enabled manifests), never scans, so a plugin install (admin) decides what env names every node may be asked. `held` is disclosed to every viewer; `runtime` is not. A node view carries `ownerLabel` (whose machine) ONLY to a cookie-admin, the `?all=1` setup-key precedent (ruling 2026-10-10); every other viewer's payload lacks the field. Daemon log over HTTP (owner/`edit`) never carries pane content or argv. `PUT /api/nodes/:id/logging` (an `edit` act, cookie only, `local` refused) is refused while `SUBSHELL_DEBUG_LOGGING` forces it on: a write the next read would mask is not a success.
- **Refused agents are HELD, not closed** (§11.12): offline for every purpose but `update` (frames dropped after envelope parse, before dispatch); the `update` command's wire shape is FROZEN across protocol bumps for this.
- **Re-registration keys**: cookie retire-gated (owner or any admin, `canRetire`) on agent nodes; the mint is stored under the node's owner so redemption's ownership check holds; `POST /api/nodes/:id/reregister` mints a single-use 24 h setup key bound to the existing row. Redemption through enroll preserves node relationships and settings, atomically swaps credentials and public identities, revokes the old bearer and unused sibling recovery keys, and evicts live/held links. Minting disconnects nothing. Deleting the target cascades recovery keys. Recovery adds no row, so `allow_node_enrollment` gates only new-node keys. Audits `node.reregister_key|reregister` carry ids, never secrets.
- **Setup keys**: single-use, 24 h, plaintext-stored to the key's OWNER only (the future node owner; for an admin-minted recovery key that is the node's owner, not the admin who triggered the mint) (cookie-only; a machine token 403s the list); `setup_key.create` carries no metadata: the key text never enters the trail. Admin may revoke ANY row (`setup_key.revoke`, `{foreign: true}` metadata), `?all=1` is admin-only (deliberate 403, never a silently-narrowed list). The node names itself (`--name`, `SUBSHELL_NODE_NAME`; `enroll` requires it) via `normalizeNodeName`, the same rule rename applies; a RECOVERY redemption validates the supplied name but does not apply it, the existing row keeps its own. Minting is gated by `allow_node_enrollment` (absent = TRUE; enforced at the mint ONLY (the key IS the credential); bounds the future, never revokes what is outstanding). `GET /install.sh` renders identically for absent/unknown/spent/expired keys (never a binary oracle); a VALID key bound to an existing node additionally names the node it re-registers and pre-supplies the wizard's name answer (the recovery path ignores any name; the row keeps its own), which discriminates between two LIVE keys only, and either one's holder can already redeem to control the node; enroll answers three distinct 401s by design. The key rides the install URL → shell history and access logs: the accepted posture of enrollment links.
- **Agent artifacts are never anonymous**: cookie, or valid unconsumed setup key, or one-time `?update_token=` (unlocks exactly one file at one digest; never reaches the `.sha256` routes). `install.sh`'s `server=<origin>` must be exact membership of the live trusted-origin registry; the rendered script digest-verifies before the first `chmod +x`.
- **Lazy release fetch**: every release-source fetch passes `releaseFetchAllowed` (configured origin ∪ GitHub release hosts; refused **by name before any connection**, egress destination pinned); no binary bytes until a machine is served (since 2026-10-09 the tiny index/manifest reads behind the serve decision run per authenticated download, TTL-cached with a one-minute failure backoff); hashed against the digest from the **signed manifest's `assets` map** (sidecar never fetched); bounded by `MAX_ARTIFACT_BYTES` (300 MB); only manifests declaring THIS `NODE_PROTOCOL_VERSION` are offered; only files this instance fetched (`.fetched.json`) are ever superseded or deleted; the shelf is reconciled AT SERVE TIME (2026-10-09: the copy is run `<file> version`, the newest server-supportable build wins, a hand-published copy speaking this protocol with a higher semver stays truth, a wrong-protocol copy 404s instead of installing a never-connectable agent, and `.sha256` announces the SAME decision the installer's skip depends on), an update download is served release-coherently else 409 naming the remedy. Empty `SUBSHELL_RELEASE_URL` = air-gapped (the shelf is the only source and answers for itself the same way: connectable and self-consistent, or refused).
- **CLI-side**: `subshell configure --server` is unprivileged, non-destructive, one-plane-two-names. It does disclose the node key to the host named (treat it as naming a host you trust). `configure --key` refuses an `nsk_` setup key by name. Re-enrolling in Subshell Client IS confirmation-gated (it overwrites `config.json` and discards the old node key).
- **The node's loopback dashboard** (§6): 127.0.0.1:3090 with **no login by design**: the listen address plus the OS user IS the access control (`Host` loopback; `Origin` when present loopback; mutations need `content-type: application/json`). The multi-user-host gap is ACCEPTED and is a widening (a second local user can maintenance-kill, stop, or repoint); `SUBSHELL_DASHBOARD=0` is the remedy. No audit rows here, the agent log is the record; `maintenance on` here kills panes, node-CLI semantics verbatim.
- **The node's loopback dashboard** (§6): 127.0.0.1:3090 with **no login by design**: the listen address plus the OS user IS the access control (`Host` loopback; `Origin` when present loopback; mutations need `content-type: application/json`). The multi-user-host gap is ACCEPTED and is a widening (a second local user can maintenance-kill, stop, or repoint); `SUBSHELL_DASHBOARD=0` is the remedy. No audit rows here, the daemon log is the record; `maintenance on` here kills panes, node-CLI semantics verbatim.
## Plugins & installers

Plugin shape and runtime facts (loaded from disk, `PluginHost`-only, JSON identity in `package.json`, static built-ins + the one sanctioned `await import()` in `plugin-runtime.ts`, plane-built argv) live in **`AGENTS.md` ("Plugins are packages…")**; that section is the source. The security half:
Expand Down
22 changes: 10 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1857,26 +1857,24 @@ jobs:
with:
bun-version: 1.4.0

# Generates from REMOTE tags (`git ls-remote`, never the API,
# site-releases.ts header). Commit with a bounded rebase retry: a cut
# runs for over an hour, and main moves under this checkout in that
# window (a merged PR, or another run's own refresh commit landing after
# this job's plan step read the tags). A third collision is not a
# (real-world shape: two pushes interleaving twice inside one second)
# so a hard failure there is the honest outcome, not a force.
# Regenerate on current main after a rejected push. This file is generated,
# so rebasing its old contents against another refresh creates conflicts.
- name: Generate, commit, verify
run: |
set -euo pipefail
bun scripts/site-releases.ts
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add releases.json
git diff --cached --quiet && { echo "releases.json already current"; exit 0; }
git commit -m "chore: refresh releases.json [skip ci]"
for attempt in 1 2 3; do
git fetch origin main
# This job owns its clean checkout and only creates generated commits.
# Leave any rejected commit behind, preserving every change on main.
git checkout --detach FETCH_HEAD
bun scripts/site-releases.ts
git add releases.json
git diff --cached --quiet && { echo "releases.json already current"; exit 0; }
git commit -m "chore: refresh releases.json [skip ci]"
if git push origin HEAD:main; then break; fi
[ "$attempt" = 3 ] && { echo "::error::releases.json push failed after 3 attempts"; exit 1; }
git pull --rebase origin main
sleep "$((attempt * 5))"
done
# Verify our own commit landed where visitors will read it, BYTE
Expand Down
Loading
Loading