cmux daemon: bridge a link share to its hosted room, 2/2: the host socket (cmux.share/1)
This PR's own diff is its last commit (+424/-24). It sits on #26825 (provisioning), which sits on feat-cmux-next.
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.3 (the host socket, reconciliation, kicks). The room is manaflow-ai/cmux-platform#6 and #8.
What it does
Each active bridged share keeps one outbound socket to {room}/v1/chat-shares/{id}/host, sent with Authorization: Bearer host_token. It uses the tokio-tungstenite client and rustls stack the cmux-tui binary already links for cmux.wire/1. No new crate enters the graph; cmux-tui-core now names tokio-tungstenite and futures-util directly.
- Reconciliation: after every
hello, the daemon's first frame isguests {active}, the guests the daemon itself holds active. Every kick the room has not acknowledged follows it. - Redeem: the registry's own atomic redeem answers
redeem {request_id, attempt_id, secret, name}, andsettleruns after it. The answer isredeemed {request_id, guest_id}, orredeemed {request_id, error}with one of the daemon'sshare.*codes only. The room passes anything outside its allowlist to the guest asshare.refused. A retry with the sameattempt_idgets the same guest. - Kick:
share-kickpersists{op: kick, guest_id}inpending_room_ops. The kick is sent at once and again on every connect until the room answerskicked {guest_id}. - Revoke, expiry or close: the daemon sends
end {reason}and closes the socket. A revoke'sDELETEruns as in #26825. - Reconnect: a dropped socket reconnects with backoff (one-shot sleeps, 1 s doubling to 60 s). It stops on close code 4000 (replaced), 4001 (ended) or 4003, and on a refused upgrade (401, 403, 404 or 410).
- Restart: the daemon resumes every active share's socket.
Not in this PR: the per-guest relay (snapshot, records, page, prompt_state, presence, guest prompts, typing). Until it lands, the room's guest frames go unanswered: a guest can join but sees nothing yet.
Risk
risk:normal: daemon-only, and inert until a share is provisioned with an owner token, which the share sheet / op PR adds. Only the room's development environment is live today. The daemon stays the authority: it names the active guests, and admission is its own redeem.
Tests
The fake room from #26825 now also speaks the WebSocket host protocol.
- Hello and redeem:
hellois answered withguestsfirst. Covered: the redeem round trip, anattempt_idretry getting the same guest,share.usedandshare.secret. After a dropped socket reconnects, the freshhellogetsguests(now naming the guest) first again. - Kick and revoke: the kick is sent after
guestsand resent on reconnect until acknowledged, and not sent again after the ack. A revoke sendsend {reason: revoked}, then theDELETEwith the host token, and no reconnect follows. - The provisioning tests from #26825.
CI receipts are in a comment below.
Changelog
none
Agent session: agent session
Room contract (cmux-platform src/server/ChatShareRoom.ts)
/hostupgrade: the room checksUpgrade: websocketandAuthorization: Bearer <host_token>only. It asks for noSec-WebSocket-Protocoland sends none back in its 101, so the daemon offers none. An offered subprotocol the server does not echo fails the handshake in tungstenite.- Answers: 404
share.not_foundand 410 (ended) mean gone (the share ends here, expired when its expiry passed); 401share.unauthorizedmeans refused for good (the share is revoked and its DELETE queued); anything else is retried with backoff. - On DELETE, expiry or purge the room sends
end {reason}and closes with 4001; a replaced host gets 4000.
š¤ Generated with Claude Code