Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: mir share / mir join — grant bootstrap over the pair room (G1b) - #101

Merged
frahlg merged 1 commit into
mainfrom
101-g1b-bootstrap
Aug 30, 2026
Merged

feat: mir share / mir join — grant bootstrap over the pair room (G1b)#101
frahlg merged 1 commit into
mainfrom
101-g1b-bootstrap

Conversation

@frahlg

@frahlg frahlg commented Aug 30, 2026

Copy link
Copy Markdown
Member

Second build slice of the G1 guest-sharing spec (§2 Bootstrap). Part of #55.

The flow as shipped

  • mir share <machine> [--ttl 1h] [--write] [--session main]: mints a one-time invite (pairing's token/code/QR + <web>/#join-<code> link), waits as room responder. The machine's identity (host_pub, machine_id, name) travels INSIDE the authenticated msg2 — nothing about the target rides in the clear.
  • mir join <code>: guest identity created on first run, room initiator; the guest claims and proves its key with pairing's own msg1/msg3, then presents its signed transport binding (verified against the claimed key before the owner ever sees a prompt).
  • Owner-only trust decision: safety number + guest short id + y/N, default No; no --yes exists; non-TTY refuses ("sharing needs a person"). --write additionally requires typing the machine name.
  • Agent learns first: the signed grant is delivered over a normal authenticated attach as CONTROL add-grant and must be acked (HELLO with ack: add-grant:<gid>) before the guest receives anything — a guest never holds a grant the machine does not know. Old agents never ack → honest abort, nothing shared.
  • Agent side receives only: VerifyGrant + signer-is-session-owner + names-this-machine + not-already-expired → persist grants/<gid>.json. Attach enforcement, expiry timers, tombstones are G1c.
  • ControlHandler's ack is now a map[string]string (rename: {name}; add-grant: {name, ack}) — rename wire behavior unchanged, tests updated.

Acceptance evidence (hermetic, httptest relay + real agent + real WebRTC attach)

  • TestShareJoinLiveGrantLandsOnAgent: mint → join → grant file on the agent verifies, names the guest identity, ro/main; guest keeps machine + grant copy.
  • TestShareDeclinedSASPinsNothing: owner answers n → join fails, agent has no grants, guest store empty, copy says "declined — nothing was shared".
  • Non-TTY refusal; --write name-mismatch cancels before any invite exists.
  • Agent handler table: foreign owner / wrong machine / expired / tampered / garbage / oversized all swallowed without ack or persistence; tmux control falls through the chain.
  • go test ./... green, gofmt -l empty, go vet clean, web 157/157. No vectors changed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KeiotDVE94wEzvc7wcvm1y


Note

Medium Risk
Touches authenticated CONTROL handling and grant persistence on the agent, but grants are strictly verified and scoped; share flow is gated on interactive owner approval and agent confirmation before the guest gets a record.

Overview
Adds time-boxed guest sharing via mir share <machine> and mir join <code>, using the existing pair room (QR/link, safety number, guest binding verification). The owner must approve interactively (no --yes; non-TTY refused; --write requires typing the machine name).

Agent learns first: after approval, the owner attaches and sends CONTROL add-grant; the agent verifies the signed grant (session owner, this machine, not expired), persists grants/<gid>.json, and re-HELLOs with ack: add-grant:<gid>. The guest only receives the grant record over the pair room if that ack succeeds—old agents that never ack abort the share.

Protocol tweak: ControlHandler acknowledgements are now a map[string]string (rename still sends {name}; add-grant sends {name, ack}), chained with rename via chainControl. Client helpers GrantOverSession and SaveGuestGrant support delivery and local guest storage; attach-time enforcement is left for G1c.

Reviewed by Cursor Bugbot for commit 4c24bf7. Bugbot is set up for automated code reviews on this repo. Configure here.

Per G1 spec §2: the guest joins the one-time NNpsk0 room as initiator
(claims and proves its key with pairing's own msg1/msg3), the machine's
info rides the authenticated msg2, the guest presents its signed
transport binding, and the OWNER holds the only trust decision — safety
number + y/N, default No, no --yes, TTY required. On confirm the grant
is minted bound to the proven guest key and installed on the AGENT
first over an authenticated session (CONTROL add-grant, acked by a
HELLO carrying "ack"), so a guest never holds a grant the machine does
not know. Write shares demand typing the machine name.

The agent only receives here: verify + session-owner + this-machine +
not-expired, then persist under gid. Enforcement is G1c.

ControlHandler acks became a map so add-grant can acknowledge without
pretending to be a rename; rename's wire behavior is unchanged.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KeiotDVE94wEzvc7wcvm1y
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-30T10:35:45.167961Z 4c24bf7 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c24bf78cd

ℹ️ 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".

Comment thread go/internal/cli/share.go
Comment on lines +44 to +46
_ = fs.Parse(args)
if len(fs.Args()) != 1 {
return fmt.Errorf("usage: mir share <machine> [--ttl 1h] [--write] [--session main]")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept flags after the machine argument

The documented form is mir share <machine> [--ttl ...], but the standard Go flag parser stops immediately before the first non-flag argument. Consequently, natural invocations such as mir share box --ttl 10m leave --ttl and 10m in fs.Args() and fail with the usage error instead of creating a share. Extract the machine positional before parsing, as cmdMachineRevoke already does, or otherwise support the advertised ordering.

Useful? React with 👍 / 👎.

Comment thread go/internal/cli/share.go
Comment on lines +84 to +85
expires := time.Now().Add(*ttl)
fmt.Fprintf(a.out, "Share %q — %s access until %s.\n", m.Name, modeWord(mode), expires.Format("15:04"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive the displayed expiry from the minted grant

This computes the advertised expiry before waiting up to five minutes for the guest and before the approval prompt, while MintGrant later sets sg.NA from a fresh time.Now(). A guest joining near the invite deadline therefore receives access for up to five minutes beyond the time shown to the owner, and both the success and disconnect messages repeat that incorrect time. Display time.Unix(sg.NA, 0) once the grant is minted, or base minting on the same timestamp used here.

Useful? React with 👍 / 👎.

Comment thread go/internal/cli/share.go
Comment on lines +227 to +233
sg, err := identity.ParseSignedGrant(verdict)
if err == nil {
err = identity.VerifyGrant(sg)
}
if err != nil {
return fmt.Errorf("the share record did not verify — ask for a new invite (cause: %v)", err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject grants that have expired before join completes

When a short --ttl is used or installing the grant consumes most of its lifetime, the grant can expire between the agent's acceptance and the guest receiving it. VerifyGrant validates only the record and signature, so this path still stores the machine and grant and prints ✓ joined even though the resulting access is already unusable. Call sg.ValidAt(time.Now()) before mutating the guest's state.

Useful? React with 👍 / 👎.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4c24bf7. Configure here.

Comment thread go/internal/cli/share.go
return fmt.Errorf("%q did not accept the share (%v) — it may run an agent from before sharing; run `%s update` on it and mint a new invite. Nothing was shared", m.Name, err, a.binary)
}
if err := mc.Send([]byte(record)); err != nil {
return fmt.Errorf("the machine accepted the share but the guest disconnected — the access expires %s on its own, or revoke id %s once share revoke ships", expires.Format("15:04"), sg.GID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grant sent in pair-room plaintext

High Severity

After the Noise handshake, cmdShare and cmdJoin send the guest binding and the full signed grant as raw pair-room bytes. Pairing discards the handshake state and never opens a transport cipher, so the relay can read machine_id, owner, guest, mode, and scope. That breaks the project invariant that the relay sees only ciphertext, and it contradicts the claim that nothing about the target rides in the clear.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4c24bf7. Configure here.

Comment thread go/internal/cli/share.go
}
if err := mc.Send([]byte(record)); err != nil {
return fmt.Errorf("the machine accepted the share but the guest disconnected — the access expires %s on its own, or revoke id %s once share revoke ships", expires.Format("15:04"), sg.GID)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pair room expires during owner consent

High Severity

Share still needs the pair room after the guest joins: the owner compares the safety number, attaches, then sends the grant. The relay’s pair-bridge TTL is 60 seconds from connect, and the share context is a single 5-minute deadline from start. A normal aloud SAS check, or a slow attach, closes the room so join fails and the agent can be left with an orphan grant.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4c24bf7. Configure here.

Comment thread go/internal/cli/share.go
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
ctx, cancel := context.WithTimeout(ctx, inviteWindow)
defer cancel()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invite window exceeds pair-room wait

Medium Severity

inviteWindow is 5 minutes and share prints that the guest has 5 minutes to join, but the relay evicts an unpaired /pair waiter after 2 minutes. A guest who arrives in that gap finds no owner, and the owner’s wait fails early despite the printed deadline.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4c24bf7. Configure here.

Comment thread go/internal/cli/share.go
}
sg, err := identity.MintGrant(signer, m.MachineID, guestWallet, *session, mode, *ttl, time.Now())
if err != nil {
return err

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grant flags validated after guest waits

Medium Severity

--ttl and --session are only checked inside MintGrant, after the guest has already joined and the owner has approved. An over-cap TTL or illegal session name aborts with nothing shared, while the guest is still blocked on the pair room until their timeout.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4c24bf7. Configure here.

@cursor
cursor Bot requested review from miravoss26 and wachtelhund August 30, 2026 10:39

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a non-blocking comment: Cursor Bugbot did not complete successfully (check skipped) and reported 4 potential issues that still need human attention, so this automation is not approving. Reviewers were assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@frahlg
frahlg merged commit 5e12695 into main Aug 30, 2026
5 checks passed
frahlg added a commit that referenced this pull request Aug 30, 2026
* assets: demo GIF for the reach-layer flow

The old demo told a story the software no longer tells: a `LAN-direct on
(mDNS + QUIC)` line for a transport deleted in #92, `wallet …` for what
v0.7 renamed to identity, and a `mir up → mir list → mir attach` flow that
predates the overview (#97), the first-run pairing QR, the tmux-style
aliases, and session sharing (#101#104).

The new take is the current one, in four beats: bare `mir` opens the live
overview; Enter attaches and the tmux session with a long-running agent is
right there; Ctrl-O d comes back, and the row now remembers what runs over
there; then one invite, read-only, gone in an hour.

Every line the script prints is a line `mir` prints. The overview screen is
overviewModel.Render()'s output, the attach banner is overview.go's, and
the whole `mir share` block — header, QR, both invite lines, the wait line
— was captured verbatim from a real run against relay.sourceful-labs.net.
The QR in the rendered GIF decodes to the join URL printed beside it. Only
the tmux screen and the shell prompt are authored, and neither is mir's
output.

The canvas grew to 1100x880 (43 rows x 102 cols) so the invite QR fits on
one screen without scrolling. 219 KB, 12.8 s.

Part of #106

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KeiotDVE94wEzvc7wcvm1y

* assets: narrow the demo canvas so every cell reads bigger

1100x880 at 102 columns left the 📱 in the mint header about 8 px wide once
GitHub scaled the GIF into its 900px column, small enough to read as a box.
No font fixes that: `fc-list ':charset=1F4F1'` finds exactly one face on this
machine, Apple Color Emoji, and a 10x crop shows that is already the glyph
being drawn — bezel, dark screen, colored icons. It is Apple's black phone on
a dark theme, not a tofu box. Naming a font in the tape only makes it worse:
every installed monospace face falls back to the same Apple glyph, and a
comma list starting with JetBrains Mono breaks the cell metrics outright,
because vhs supplies that face as a webfont and the list form defeats it.

So the lever is columns, not fonts. 884x878 gives 42 rows x 80 columns: the
last beat needs 41 rows, and 80 is the narrowest width that still wraps each
invite URL to two lines rather than three. Every cell — the emoji included —
grows about a quarter, and the frame stops being mostly empty.

Widths must stay even. 883 defeated the palette pass and turned the same
recording into 14 MB; 884 renders it at 214 KB.

214 KB, 12.8 s, 884x878. The QR still decodes to the join URL printed
beside it.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KeiotDVE94wEzvc7wcvm1y

---------

Co-authored-by: Claude Fable 5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant