cmux daemon: link shares for a guest (create, list, revoke, kick, redeem)
Leo (2026-10-28), on T3 Code's "Share with guest": "we want to be having, be able to do this kind of flow for CMux multiplayer ... we can work on the agent chat multiplayer."
The flow: the owner creates a link that works once and expires in 24 h. A guest opens it in a browser, types a name and watches the chat live. Every message the guest sends waits for the owner's approval. Inventory and plan: manaflow-ai/cmuxterm-hq#4766. Design: chat-multiplayer.md section 4.2.
This is part 1 of 2: the daemon's share registry and its verbs. Part 2 is the guest binding: agent-session-proxy-bind {link_guest}, the guest's allowed verbs, and the one-shot expiry timer that closes guest connections. It is split out to keep each PR under 800 lines.
What it does
- Registry. The daemon keeps every share in
agent-session-shares.json, next to the session's registry. The file is owner-only and rewritten atomically, using the same writer and trusted reader as the agent-transfer records.- A share is pinned to the chat's acpmux session id, not to the surface number.
- It records
uses_left,state(provisioning|active|revoked|expired|closed), its guests (active|kicked|left, and each guest'sattempt_id_sha256), androom,provision_keyandpending_room_opsfor the room bridge. - It stores only the secret's SHA-256.
- At most 16 records are kept, which bounds the file.
- A restart reads the file back, so kicks, revokes and spent uses still hold.
- Verbs. They answer only the owner's own same-user unix connection. A remote relay, a WebSocket or any bound proxy link gets
share.not_trusted(a teammate's link getsagent_session.not_trustedfrom the existing scope check).agent-session-share-create {surface, ttl_secs? (default 86400, 60 to 7 days), uses? (default 1, 1 to 8)}returns{share_id, secret, url: null, expires_at, session, surface, uses_left}.share_idandsecretare 128-bit base64url. The secret is returned only in this reply.agent-session-share-list {surface?}lists the shares with no secret or hash. Each has its guests,stateandexpires_in_secs.agent-session-share-revoke {share_id}andagent-session-share-kick {share_id, guest_id}persistrevokedandkicked.agent-session-share-redeem {share_id, secret, name, attempt_id}:- Checks run in this order: the share exists, the secret hash matches (constant time), the share is live and not expired, a use is left. The name must be one line of 1 to 64 characters, using the same rule as acpmux's
author.rs. - On success it spends one use and creates a 64-bit
guest_id. This happens atomically under the registry lock, and the change is saved before the reply. - Repeating a redeem with a recorded
attempt_idreturns the same guest and spends no use.
- Checks run in this order: the share exists, the secret hash matches (constant time), the share is live and not expired, a use is left. The name must be one line of 1 to 64 characters, using the same rule as acpmux's
- Expiry is checked on every use: a share past
expires_atreads asexpired. - Refactor: the teammate verb allowlist moved from
agent_session_attach.rsintoagent_session_proxy.rs(scope).scopealso routesagent-session-ofand the share verbs, so the god file shrinks (1325 to 1309 lines).
Error codes: share.unknown, share.secret, share.revoked, share.expired, share.provisioning, share.closed, share.used, share.kicked, share.name, share.limit, share.not_a_chat, share.not_trusted, share.unsaved (the write failed and the change was rolled back), share.bad_request.
Risk
risk:high: socket policy. These are new owner-only daemon verbs. They grant nothing yet, because no connection can act as a guest until part 2. A teammate's link keeps exactly its six verbs; the allowlist only moved. Not done here: a CLI (cmux share chat|list|revoke|kick; design PR 6). Until then, run cmux raw command --request-json '{"cmd":"agent-session-share-create","surface":N}'.
Tests
cargo test -p cmux-tui-core agent_session_shares (agent_session_shares_tests.rs):
- create, list, kick, revoke, and refusals for a non-chat surface and bad params;
- two concurrent redeems of a one-use link: exactly one wins and the other gets
share.used; a wrong secret spends nothing; a retriedattempt_idgets the same guest after the link is spent; names are validated; - expiry on use, with an injected clock;
- shares, uses, kicks and revokes survive a reload from disk, and the file is mode 0600;
- remote-relay, WebSocket and teammate-bound connections cannot create or redeem.
Results from cmux-ci follow in a comment.
Changelog
none
Agent session: agent session
š¤ Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Adds the daemon-side share registry for agent-session-share-* verbs, letting an owner create once-only, expiring links for an agent chat. Guests can redeem share links and receive a guest_id; the guest binding itself ships in a follow-up.
- Shares persist in
agent-session-shares.json(owner-only, atomic rewrite, directory synced before the reply) and survive restarts, including used-up limits, kicks, and revokes. - Verbs only work on the owner's own same-user unix connection; remote relays, WebSockets, and teammate-bound links are refused.
- Redeem spends one use atomically and returns the same
guest_idon a retriedattempt_id; a kicked or left guest's retry stays refused. Expiry is checked on every use. - Max 16 shares, TTL 60sā7 days, uses 1ā8; only the SHA-256 of the secret is stored.
- Refactors the teammate verb allowlist into
agent_session_proxy.rs(scope), shrinkingagent_session_attach.rs.