Scoped Agent Sessions
multiplayer-agent-sessions / v1 proof of concept

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.

SESSION #42 · viewing as

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.

    terminal
    # 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.