Shared agent session · per-viewer permissions
Everyone in the session. Not everyone sees everything.
Three teammates watch the same live agent. It uses each person's own integrations, and the server redacts every tool call a viewer doesn't own, per viewer, before it leaves the machine.
Watch the demo
Four minutes, recorded on the real app: Nina and Theo side by side, then Sam joining as a contractor.
Two things break when a second person joins an agent session
Tools that make AI multiplayer mostly sync history and memory across a team. Two problems show up the moment people share one live session. As far as their public launch material shows, neither has a per-viewer boundary yet.
The agent's output is only as private as its most-connected user
Nina connects her support inbox. Theo joins and scrolls up. Everything the agent read from Nina's email is on his screen, even though he never had access to it.
FixTag every tool call with the integration it used, and filter the stream per viewer against a permission map.
"Who's on it" is tracked per session, not per ticket
Two people ask the agent to resolve the same ticket a second apart. Both drafts get approved, and the customer gets two answers, one of them stale.
FixA claim-lock keyed on the ticket. The first claim reaches the agent; the second gets told who has it.
“When adding another teammate to a session that has exposure to sensitive data, it is important to handle permissions correctly.”
Privacy pitfalls, The Multiplayer AI Manifesto, September 2026
Race two people to the same ticket
Click Resolve in either seat, or race them. The broker handles one claim at a time, so the first one wins and only that one is sent to the agent.
Broker log
One shared process, filtered at the broker
Built on wetty (node-pty, xterm.js, socket.io). Its server sees terminal output in plain text, which is exactly what server-side redaction needs. An end-to-end encrypted terminal couldn't filter anything.
Tag
Each mock tool prints its call as one marker line. The broker parses it out of the stream and never forwards the raw marker.
##TOOL_CALL##{"tool":"send_email",
"owner":"support","output":"…"}
Filter per viewer
For each connected browser, the broker checks the permission map. Owners get the output; everyone else gets a redaction line.
if (!viewer.ownedTools.includes(call.tool)) return redacted(call.tool);
Lock before acting
Viewers steer the agent only through claims. A won claim is written to the agent's stdin; a lost one never reaches it.
const held = tryClaim('ticket-42', 'sales');
if (held) reject(held.claimedBy);
browser ─ join / claim ─▶ session broker ─ resolve ticket-42 support ─▶ pty stdin ─▶ agent each browser ◀─ output, redacted for that viewer ─ session broker ◀─ pty stdout ◀──────┘
Run it on your machine
Node 20+ and pnpm. Open two or three tabs and pick a different person in each. The agent is a scripted stand-in so the demo runs the same every time.
# clone, then: pnpm install pnpm build pnpm start # open http://localhost:3000 in two tabs
Deliberately not in v1
- Real integrations. Gmail, Slack and a CRM with real OAuth scopes instead of mock tools.
- A real agent CLI in the pty instead of the scripted stub.
- Login. v1 uses “pick your name” per tab.
- Persistence. Claims and history live in memory; a restart resets the session.
- Output-bound claims. Tie a claim to a hash of the draft, so an edited draft invalidates a stale approval.