host share (daemon): a bound teammate connection reaches only the pinned chat
Leo, 2026-10-26: "we can still do the regular chat multiplayer". Earlier: "also chief chats we need multiplayer". Relayed: "for the agent chat side, we should be able to just... do stuff. unless lawrence is super actively workin on stuf rn"
Design: https://github.com/manaflow-ai/cmuxterm-hq/pull/4389, section 2 (who can join, enforcement) and section 5 (drafts).
This is the daemon half of the shared-chat security boundary, split out of #25300 to keep each PR reviewable. #25300 keeps the host proxy, the share list and the CLI, and lands after this one. On its own this PR reaches nobody new: no teammate can connect until #25300's proxy lands. Every new rule applies only to a connection that bound a teammate identity, and only a same-user Unix client (which already has owner access) can bind one.
What
agent-session-proxy-bind {author:{login, node}}(agent_session_proxy.rs). It makes a same-user Unix connection the host proxy's link for one teammate. It is accepted only as the connection's first line (line_connection.rsmarks the rest), so a link never carries a subscription or other daemon state into a teammate's session. It is refused on a WebSocket or remote-relay connection, a second time, with any_host_field, and once the connection has an attachment. The daemon fixes the author to{id: "tailnet:<login>", name, login, node}. The reply carries the protocol and boot only: no paths and no session inventory.- Every frame of a bound connection is checked (
proxy::check)._host_authormust equal the bound author. Every frame except detach must also carry_host_session, the chat the owner shared, and the surface must show that chat now: a tab rebound to another chat is refused as the newagent_session.not_shared. An unbound connection that sends either field is refused (agent_session.not_trusted), so nobody can claim an author. - What a bound (teammate) attachment gets:
- every acpmux call stamped with
_meta.acpmux.author(attach, the session watch, events, prompt, cancel); - only the
transcriptkinds, whatever it asks for; - no
composerrecords (drafts), in the replay, history pages or live; - no descendant chats:
agent-session-childrenis refused and no child change is streamed; - the session summary cut to what the chat shows. A change keeps only
sessionId,kind,recordKind,seqand that cut summary; - no
agent-session-permission: a teammate never answers the host's tool asks.
- every acpmux call stamped with
- New in this split:
agent-session-typingandagent-session-view(added on base by #25375 since #25300 was written) are refused for a teammate. acpmux would count those as the owner's own presence. A teammate attachment also gets nopresenceset or event, since that names who else has the chat open. Teammate typing and view through the proxy come in a later PR, with a design for how acpmux attributes them. - A bound connection reaches the six shared-chat verbs only (attach, observe, events, prompt, cancel, detach). The connection dispatch (
connection_frame.rs) sends every frame of a bound connection to the agent-session handler, which refuses anything else asagent_session.not_trusted: start, trust, media, children, permission, typing, view,-of, and any other daemon command, resource or page line. A_host_field other than the proxy's two is refused on any connection. Every request uses the chatproxy::checkvalidated, carried to attach, observe and attached commands, never a later read that a rebind could change. A page acpmux answers after the tab was rebound is refused asagent_session.not_shared, on attach and on history pages. agent-session-of {surface}answers{surface, session}, socmux host share(#25300) can pin the chat. Only the owner's own unbound same-user Unix connection may ask.spec/commands.mddocuments all of the above under "Agent session attach".
What a teammate can and cannot reach (once #25300 lands)
The daemon side of the paragraph in #25300: a bound connection reaches only the agent-session-* verbs on a surface that shows the pinned chat, sees only that chat's transcript records and cut summary, and acts in that chat only as attach, observe, events, prompt, cancel and detach. It cannot answer asks, type, view, list descendants, read drafts or presence, or name another chat. A share written before #25300's pinning commit has no session and never matches; it must be shared again.
Size
+1116/ā28 across 8 files, about 650 of them tests and 70 spec. That leaves under 400 lines of code, all on the bound-connection boundary. It is over the ~800 cap because the security reviews asked for a test per property, and those tests belong with the code they guard.
Risk
Low on its own: nothing calls bind until #25300. It's a restriction-only layer for bound connections; owner connections behave as before (the unbound path is Ok(None) and every check is skipped).
Tests
Run through cmux-ci test --setup rust (Linux):
cargo test -p cmux-tui-core agent_session: 71 passed;- clippy (
-D warnings) and fmt clean.
New and moved tests:
- the proxy identity is fixed and stamps acpmux calls;
- a teammate never follows a rebound surface to another chat;
- a teammate attachment never sees drafts, descendants, presence, or the summary's folder or tags, and cannot answer, type or view;
- a teammate that disconnects mid-attach still has its author on the watch;
- a WebSocket connection cannot bind, and a remote-relay connection can neither bind nor read a surface's session;
- a teammate's link reaches the six verbs only (start, trust, media, children, permission, typing, view, list-workspaces, identify and subscribe refused; a forged
_host_fakerefused for owner and teammate); - a rebind while acpmux answers a teammate's attach, a repeated attach or a history page refuses the page;
- a teammate's request uses the validated pin (an attach validated for another chat is refused);
- a bind is accepted only as the first line, and never with a host marker.
Checked on CI to fail without the typing/view refusal, the presence gate, the dispatch gate, the rebind recheck, the pinned chat, the page recheck and the first-line rule.
Daemon-side only, so there are no screenshots.
Agent session: agent session
š¤ Generated with Claude Code