cmux daemon: bridge a link share to its hosted room, 1/2: provisioning (cmux.share/1)
Leo (2026-10-28), on Theo's T3 Code "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." A guest opens a link in any browser, joins with a name, watches live, and each message waits for the owner.
Tracks manaflow-ai/cmuxterm-hq#4766. Spec: chat-multiplayer.md 4.2 (create, provisioning, pending_room_ops) and 4.3 (the room's PUT/DELETE). The room itself is manaflow-ai/cmux-platform#6 and #8.
This is the first of two PRs. It sits directly on feat-cmux-next, now that #26750 and #26758 have landed. The second half, the host socket, is #26827, stacked on this one.
Size: 1,278 changed lines stay together because this PR is one share lifecycle state machine with its tests.
What it does
When a room is configured, agent-session-share-create provisions the share in cmux-platform's ChatShareRoom before it answers:
- The share is persisted as
provisioning. - The daemon sends
PUT {room}/v1/chat-shares/{id}withAuthorization: Bearer <owner_token>and{provision_key, secret_sha256 (lowercase hex SHA-256 of the secret's UTF-8), expires_at, uses}. - The daemon stores
room {url, host_token}in the share's 0600 record and the share becomesactive. - Create answers
url = url_base#secret.
share-list shows the room's origin and never shows the host token.
Failures:
- No answer: create answers
share.room_unreachable. ThePUTis retried in the background with the sameprovision_keyfor 10 minutes, using one-shot sleeps that start at 1 s and double up to 60 s. After that the share isrevokedand a roomDELETEis queued inpending_room_ops. - The room answers after create already failed: the room is deleted with the new host token. Its link was never handed out.
- 401:
share.needs_sign_in. - Any other 4xx:
share.room_refused, and the share is revoked. - Restart while
provisioning: the share is revoked.
Revoke queues the room's DELETE. It is retried until the room answers or the share expires, and a restart resumes it.
The owner token comes from the app. The daemon has no cmux cloud token of its own: the cloud-session-set lease is the Stack session token, while the room verifies a cmux-api install token (ES256, aud api, with a kid). So create takes owner_token, holds it in memory only for the PUT (and the give-up DELETE), zeroizes it, and never writes it to disk. When a room is configured, a create with no owner_token answers share.needs_sign_in.
The room URL is CMUX_SHARE_ROOM_URL in the daemon's environment; an empty value turns links off. When it is unset, each build uses the room of the cmux API it signs in to:
| Build | Room |
|---|---|
| DEV | https://cmux-platform-development.debussy.workers.dev |
| staging, NIGHTLY | https://cmux-platform-staging.debussy.workers.dev |
| release, RC | https://cmux-platform-production.debussy.workers.dev |
| standalone daemon (no app bundle) | none: url stays null, as before |
The room ships inert: only development is live for now. Staging and production answer 404 share.not_found, which create reports as share.room_refused until they are enabled.
Risk
risk:normal: daemon-only. Nothing calls create with owner_token yet; the share sheet / op PR will. Until then, a create on a bundled build with a room answers share.needs_sign_in instead of a share with no URL, and the CLI/op PR passes the token.
The owner token is held only in memory, behind Zeroizing, and is never logged or stored. The host token stays in the 0600 share file, as the spec says.
Tests
agent_session_share_room_tests.rs runs against a fake room: an in-process HTTP server that records every request.
- Provision success: the PUT path, the bearer, and the body (
secret_sha256matches the secret; uses; expiry; a 22-character key); the URL;active; no host token inlist. A revoke then sends exactly oneDELETE, with the host token. - An unreachable room: the
PUTis retried with the sameprovision_key, then the share is revoked and theDELETEuses the owner token. - A room that answers after create failed: it is deleted with its host token and the share is revoked.
- Refusals: no
owner_tokengivesshare.needs_sign_in; a 409 givesshare.room_refusedand a revoked share; a 401 givesshare.needs_sign_in. NoDELETEis sent for any of them. - Room per build:
share_room::each_build_kind_has_its_room.
The existing agent_session_shares_tests cover the room-less path, which is unchanged.
CI receipts are in a comment below.
Changelog
none
Agent session: agent session
š¤ Generated with Claude Code