feat(cli): share surface — mir share ls/revoke, guest state, overview (G1d) - #103
Conversation
… (G1d)
Per G1 spec §6. Owner side: every mint is recorded locally so `mir share
ls` lists it (short id, machine, mode, scope, expiry countdown, revoked)
and `mir share revoke <id-prefix>` delivers the tombstone over an
authenticated attach and flips the record. Revocation stays agent-local
(§4): an unreachable machine means the revoke has NOT happened — the
copy says so plainly, and says the honest counterpart too: while the
machine is offline the guest cannot reach it either.
Guest side: `mir ls` renders a share as what it is ("shared with you ·
read-only · expires in 42 min"), attach checks the grant's clock before
dialing (an expired share earns a clear line, not a fake "offline"),
and shares whose window has fully closed age out of the local store.
Overview: a share renders as ⇢ with the grant summary; `s` on an owned
machine drops to the mint ceremony and returns; rename/retire/share on
a share answer with a quiet hint instead of acting.
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. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7b4fdd2899
ℹ️ 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 := a.requireRootedIdentity(idn); err != nil { | ||
| return err | ||
| } | ||
| machines, err := a.resolveMachines(context.Background(), *dir, []string{share.MachineName}, idn) |
There was a problem hiding this comment.
Resolve revocations by stable machine ID
If the machine is renamed after a share is minted, MachineName retains the old display name while resolveMachines only resolves current names, so mir share revoke <id> reports an unknown machine and the guest keeps access until expiry. Resolve the target using the stable share.Grant.Machine ID (and use the current name only for display) so supported machine renames cannot disable revocation.
Useful? React with 👍 / 👎.
| if g := client.GuestGrantFor(ov.dir, m.MachineID); g != nil { | ||
| row.WindowsLine = fmt.Sprintf("shared with you · %s · %s", modeWord(g.Mode), expiryPhrase(g.NA, false, time.Now())) |
There was a problem hiding this comment.
Check shared grants before overview attach
When a guest opens the overview after a grant expires and presses Enter on this shared row, the overview's attach path bypasses the expiry check added to cmdAttach. It therefore enters the reconnect loop and retries the agent's refusal for roughly the full failure budget instead of immediately explaining that the share ended; validate GuestGrantFor(...).ValidAt(...) before making the row attachable.
Useful? React with 👍 / 👎.
| func SweepGuestState(dir string, now time.Time) { | ||
| cutoff := now.Add(-identity.GrantSkew).Unix() | ||
| live := map[string]bool{} | ||
| for _, g := range ListGuestGrants(dir) { |
There was a problem hiding this comment.
Abort guest sweeping when grants cannot be read
If the grants directory or an active grant file has a transient read error, ListGuestGrants silently returns or omits that grant, and this loop consequently leaves its machine out of live; the following loop then permanently removes the still-active guest machine from machines.json. Make grant loading return errors and skip the destructive sweep unless the grant store was read successfully, removing entries only when their expiry was positively established.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 4 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 7b4fdd2. Configure here.
| if row.Shared { | ||
| ov.model.Status = "that's a share — it expires on its own; nothing to retire" | ||
| break | ||
| } |
There was a problem hiding this comment.
Rename prompt drops the letter s
Medium Severity
handlePromptKey never treats ovShare as typed input, so the new s binding is swallowed inside the rename prompt. Any name that contains s is stored without those letters.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7b4fdd2. Configure here.
| ov.model.Status = err.Error() | ||
| default: | ||
| ov.model.Status = "shared — `" + a.binary + " share ls` lists it" | ||
| } |
There was a problem hiding this comment.
Overview share reports success on decline
Medium Severity
Overview share() treats a nil return from shareResolved as a completed mint. Canceling at the safety-number prompt also returns nil, so the status bar claims the machine was shared when nothing was granted.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7b4fdd2. Configure here.
| if row.Shared { | ||
| ov.model.Status = "that's a share — it expires on its own; nothing to retire" | ||
| break | ||
| } |
There was a problem hiding this comment.
Overview attach skips expired-share check
Medium Severity
cmdAttach now refuses an expired guest grant before dialing so the agent’s silent drop is not mistaken for “offline”. Overview Enter still calls client.Attach with no ValidAt check, so the default mir picker keeps showing that misleading failure.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7b4fdd2. Configure here.
| if err != nil { | ||
| return err | ||
| } | ||
| m := machines[0] |
There was a problem hiding this comment.
Revoke breaks after a machine rename
Medium Severity
cmdShareRevoke resolves the target with share.MachineName, a display name frozen at mint. After machine rename, that name is gone, so the tombstone never reaches the agent and the guest keeps access until TTL.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7b4fdd2. Configure here.




The CLI surface of guest sharing (G1 spec §6). Part of #55.
Owner side
<dir>/shares/<gid>.json(the agent's store stays the authority).mir share ls:mir share revoke <id-prefix>— unambiguous-prefix resolution, tombstone delivered over an authenticated attach (G1c'sRevokeGrantOverSession), local record flipped, idempotent re-run says "already revoked".Guest side
mir ls: a share renders assharebox <id> shared with you · read-only · expires in 42 min— never as an owned machine.mir attachchecks the grant's clock before dialing: an expired share gets "your share of X has ended — ask the owner for a new invite" instead of the agent's silent refusal reading as "offline".mir ls(machine entry + grant file).Overview (O1)
son an owned machine leaves the alt screen + raw mode, runs the mint ceremony (defaults: ro, 1 h, main — flags need the command form), and returns to the overview; hint bar now readsenter attach · s share · r rename · x retire · q quit · ? help.s/r/xon a share answer with a quiet status hint (only the owner shares onward / renames; it expires on its own).Tests — the full live loop on the hermetic harness: mint → join → guest ls flagged →
share lsshows the id →share revoke <prefix>lands on the live agent → local flag → idempotent re-revoke. Plus: prefix resolution (unknown/ambiguous), sweep table (live kept, closed removed, owned untouched), expired-share attach refusal (no dial), overview shared-row render, expiry phrasing table. G1b/G1c E2E suites stay green as the gate.go test ./...green,gofmt -lempty,go vetclean, web 157/157; no vector changes;mir doctoruntouched.🤖 Generated with Claude Code
https://claude.ai/code/session_01KeiotDVE94wEzvc7wcvm1y
Note
Medium Risk
Touches guest access paths (attach pre-checks, revoke over live sessions, local grant bookkeeping); agent remains authoritative but mis-revoke or stale local state could confuse users until re-sync.
Overview
This PR completes the G1d CLI surface for time-boxed guest sharing: owners can see and end invites; guests see shares distinctly and get clear errors when access has ended.
Owners get
mir share ls(mints recorded under localshares/) andmir share revoke <id-prefix>with prefix resolution, agent tombstone viaRevokeGrantOverSession, and idempotent “already revoked” handling. Offline revoke is explicit: the agent must ack; unreachable machines get honest copy about TTL and guest reachability. Successful mints are persisted locally; help text documents the new subcommands.Guests see shared machines in
mir lsand the overview as “shared with you” with mode and expiry (not relay URLs).mir attachvalidates the grant clock before dialing so expired shares fail locally.SweepGuestStateon list drops closed grants and guest machine pins.The machine overview adds
sshare (mint ceremony with defaults, exiting alt-screen/raw mode), ⇢ rows for shares, and blocks share/rename/retire on guest entries. Share mint logic is factored intoshareResolvedfor reuse from CLI and overview.New
client/shares.goplus tests cover owner records, guest grant lookup, sweep, and live ls/revoke/attach/overview coverage.Reviewed by Cursor Bugbot for commit 7b4fdd2. Bugbot is set up for automated code reviews on this repo. Configure here.