feat: machine rename from SPA + CLI via the encrypted registry (N1) - #67
Conversation
…istry (N1)
The agent never holds the owner root, so it cannot seal registry records.
A rename therefore starts on an owner client: it renames locally at once
(instant effect), re-seals the discovery record under the owner root, and
delivers name + blob to the machine over the authenticated session as an
agent-level CONTROL command ({a:"rename-machine"}). The agent validates,
persists both, republishes the record on its live relay registration (the
relay replaces the blob it holds — still blind, still stateless), and
re-HELLOs the session as the acknowledgement.
Other devices converge through the registry: records already carry ts, so
the name merge becomes last-writer-wins on a new local name_ts (Go and JS
mirrored). A local rename not yet delivered keeps winning; a newer registry
record (a rename made elsewhere) now updates local entries — previously the
local name always stayed, which would have pinned stale names forever.
- agent: ControlHandler seam in RunAgentSession (rename claimed before tmux
control); RenameMachine persists name + owner blob; live signaling writers
registry for mid-run republish; serveAuthenticated learns the owner id
(relay + LAN paths).
- client: `mir machine rename <name> <new-name>` (local-first, then deliver
+ await the HELLO ack, honest errors when offline/unconfirmed);
RenameLocalMachine keyed by machine_id; SealRegistryMachine returns its ts.
- web: ✎ rename in the terminal topbar (seal + sendCtl + optimistic local
upsert); late HELLOs update the title live (renames from other devices);
mergeMachines carries name_ts.
Closes #63
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeiotDVE94wEzvc7wcvm1y
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
…ct + gofmt) session.go keeps both R3's hook-trigger window push and N1's ControlHandler; app.js's connectOnce takes onLink (R1, 6th) AND onHello (N1, 7th). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeiotDVE94wEzvc7wcvm1y
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 59ef0eb3bf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if err := json.Unmarshal(data, cfg); err != nil { | ||
| return err | ||
| } | ||
| cfg.MachineName = name |
There was a problem hiding this comment.
Preserve the renamed name across agent restarts
On the normal mir up restart path, cmdUp calls LoadOrInit with the hostname/default --name, and LoadOrInit unconditionally assigns that value to cfg.MachineName. Consequently, the name persisted here is overwritten whenever the agent restarts unless its launch configuration is also manually changed; subsequent HELLOs and newly paired owners see the pre-rename/default name. The startup path needs to preserve a persisted rename when --name was not explicitly supplied.
Useful? React with 👍 / 👎.
| machine = { ...machine, name: n, name_ts: sealed.ts }; | ||
| addMachine(machine); // upsert by machine_id — the local rename is instant | ||
| titleEl.textContent = n; | ||
| ctl({ a: 'rename-machine', n, blob: sealed.blob }); |
There was a problem hiding this comment.
Confirm or queue the SPA rename before committing it locally
In the inspected SPA terminal flow, the rename button remains usable while connecting, reconnecting, or failed, but ctl silently does nothing when current.send is null; an older agent similarly ignores this new command. These lines nevertheless persist and display the new local name without waiting for the promised HELLO acknowledgment, leaving the machine and encrypted registry on the old name indefinitely. Retrying the desired name is also blocked by the earlier n === machine.name return, so delivery must be queued or failed/unconfirmed renames must remain retryable.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b8950ef. Configure here.
| cfg.OwnerRegistry = make(map[string]string) | ||
| } | ||
| cfg.OwnerRegistry[ownerID] = blob | ||
| return save(dir, cfg) |
There was a problem hiding this comment.
Persisted rename lost on restart
High Severity
RenameMachine writes the new display name into config.json, but every mir up (and mir pair responder / mir enroll) calls LoadOrInit, which always overwrites MachineName with the --name flag. That flag defaults to hostname(), including LaunchAgent and auto-update restarts, so the persisted rename is replaced on the next agent start. New sessions then HELLO the old hostname while the sealed registry blob can still carry the new name.
Reviewed by Cursor Bugbot for commit b8950ef. Configure here.
| machine = { ...machine, name: n, name_ts: sealed.ts }; | ||
| addMachine(machine); // upsert by machine_id — the local rename is instant | ||
| titleEl.textContent = n; | ||
| ctl({ a: 'rename-machine', n, blob: sealed.blob }); |
There was a problem hiding this comment.
SPA rename drops when disconnected
Medium Severity
renameMachineUI always upserts the local store and title, then calls ctl, whose sendCtl no-ops when current.send is null (connecting, reconnecting, or a dead session). There is no HELLO wait, unlike the CLI. The receive loop also feeds the agent’s initial HELLO into onHello, so a later attach overwrites the optimistic title with the still-old agent name and never retries delivery.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit b8950ef. Configure here.




Closes #63. Slice N1 of the v0.8-beta UX roadmap: rename a machine on one device and the new name appears on every device — relay still blind, still stateless.
The propagation design (and why)
The agent cannot seal registry records — since the v0.7 hardening the owner client seals the discovery blob during pairing and the agent only republishes it opaquely (targets are expendable). So the only sound path for a rename is owner-side:
ts.{a:"rename-machine", n, blob}as an agent-level CONTROL command through the existing authenticated channel. No new frame type, no new relay route.setRegistryalready replaces per registration — zero relay changes), and re-HELLOs the session — the client's acknowledgement.ts, so the name merge is now last-writer-wins onname_ts(Go and JS mirrored, tests on both). A local rename not yet delivered keeps winning; a newer registry record updates local entries.Alternatives weighed: a client→relay write endpoint (rejected: new route + breaks "the blob rides the live registration"); rename as local alias only (rejected: never propagates); a new frame type (rejected: CONTROL already carries verb-style JSON both languages understand).
Behavior change (deliberate)
MergeMachinesused to keep the local display name unconditionally — under rename that would pin stale names forever. Registry names now win on newer-or-equalname_ts. Pairing-time names are identical on both sides, so nothing visibly changes for existing users; a hand-editedmachines.jsonname will revert to the registry name until renamed properly.Surface
mir machine rename <name> <new-name>— local-first, then delivery + HELLO-ack wait, with honest errors: unreachable machine → "renamed locally, other devices keep the old name"; no ack → "the machine may run an older agent".Left out (deliberately)
Tests
go test ./...green. New: name validation; persist (only the requesting owner's slot); handler happy-path incl. live republish on a fake signaling writer; bad-input swallowing; a full session-level test that the CONTROL round-trips to a fresh HELLO over Noise; merge LWW table; local upsert. One existing merge test updated to the new name rule (it pinned the old one).npm test119/119. New: merge LWW table; sealMachineRecord → decodeRegistry round-trip carryingts; existing merge/decode tests updated forname_ts.testdata/vectors changed (no handshake/derivation code touched).Conflict note: touches
go/internal/agent/session.go(the CONTROL case + one new parameter) — coordinate with R3 (#57) at merge; the hunks are in different regions.🤖 Generated with Claude Code
https://claude.ai/code/session_01KeiotDVE94wEzvc7wcvm1y
Note
Medium Risk
Touches authenticated CONTROL handling, local/registry merge semantics (stale hand-edited names can revert), and multi-step rename delivery; crypto and relay paths are unchanged but behavior is user-visible across devices.
Overview
Adds end-to-end machine rename so a new display name on one device propagates to every paired client through the existing encrypted registry—no relay API changes.
Agent: Authenticated sessions accept a CONTROL command
{a:"rename-machine", n, blob}(owner-resealed registry blob). The agent validates the name and blob size, persistsmachine_nameplus that owner’s registry slot, republishes the blob on the live signaling WebSocket, and re-sends HELLO as acknowledgement.serveAuthenticatednow passes owner id into a per-sessionrenameHandler; HELLO uses a livemachineName()that can change mid-session.Clients (Go + web):
mir machine renameand the SPA terminal ✎ button update localmachines.jsonfirst withname_ts, re-seal the discovery record, attach, and wait for confirming HELLO (RenameOverSession).SealRegistryMachinenow returns the record timestamp;MergeMachines/mergeMachinespick display names by last-writer-wins onname_ts(registry wins when newer)—replacing the old rule that always kept the local name.Session plumbing:
RunAgentSessiontakes an optionalControlHandlerthat runs before tmux CONTROL handling; successful rename triggers an extra HELLO frame. The web attach path handles late HELLOs to refresh the title when another device renames.Reviewed by Cursor Bugbot for commit b8950ef. Bugbot is set up for automated code reviews on this repo. Configure here.