cmux daemon: bridge a link share to its hosted room, 1/2: provisioning (cmux.share/1)

Merged
TE
8d ago
+1516-16

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:

  1. The share is persisted as provisioning.
  2. The daemon sends PUT {room}/v1/chat-shares/{id} with Authorization: Bearer <owner_token> and {provision_key, secret_sha256 (lowercase hex SHA-256 of the secret's UTF-8), expires_at, uses}.
  3. The daemon stores room {url, host_token} in the share's 0600 record and the share becomes active.
  4. 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. The PUT is retried in the background with the same provision_key for 10 minutes, using one-shot sleeps that start at 1 s and double up to 60 s. After that the share is revoked and a room DELETE is queued in pending_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:

BuildRoom
DEVhttps://cmux-platform-development.debussy.workers.dev
staging, NIGHTLYhttps://cmux-platform-staging.debussy.workers.dev
release, RChttps://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_sha256 matches the secret; uses; expiry; a 22-character key); the URL; active; no host token in list. A revoke then sends exactly one DELETE, with the host token.
  • An unreachable room: the PUT is retried with the same provision_key, then the share is revoked and the DELETE uses 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_token gives share.needs_sign_in; a 409 gives share.room_refused and a revoked share; a 401 gives share.needs_sign_in. No DELETE is 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