Skip to content
This text was generated by an LLM (how cgwalters uses LLMs). cgwalters has not reviewed it yet.

Interactive sessions

Partly built: a session run (session runs) goes on after its task's turn to an operator's prompts, through a bridge that listens on the runner's tailnet name behind tailscale serve. What follows is the earlier design note: a person, or another program, attaches to the agent's session and works with it, while the agent stays inside the same sandbox, inference proxy and safe-outputs limits as an unattended run. It is why the agent is driven over ACP.

The operator's words: "Another use case I have btw is interactive chats this is why ACP. We just mix in Tailscale or equiv. Should be a clean mode flag to wait for a connection. Or start from a prompt but accept dynamic connections."

The plan defers this work. The proposed first bridge is a Unix socket on the runner, reached by an attach command over Tailscale SSH that copies its standard input and output to the socket: ACP stays a byte stream, nothing new listens on the network, and the tailnet's own identity decides who gets in. Everything below is a proposal.

What exists

One seam, in session/clients.rs. The session holds ACP's Client role toward the agent. Every message of the agent's passes one tap, which records it and hands the agent's notifications to a list of attached clients. The list is empty for a task run; in a session run it holds the bridge's one client, which the spec describes with its trust boundaries.

The ACP SDK's Proxy role is not what this builds on. It speaks _proxy/* to a conductor that owns both of its connections, and a run with no client side has nothing for a conductor to own.

Still to design

The session spec settles the way from a client to the agent (prompts and cancels over the bridge), who answers a permission request when a person is watching (the run's policy, still), how a prompt sent during a turn is held (it is refused: Claude Code's adapter would end the running turn for it) and what ends a session with a client (the client leaving, or an idle timeout). Still open is a client that joins a session already running: the bridge's client is attached when the session starts.

Who may attach

A client is a new source of instructions with the authority of whoever is typing, but it cannot widen the run: the safe-output types, the target, the egress policy and the limits are fixed before the agent starts, and nothing sent over ACP changes them.

This assumes run without --runner-home, where the agent is a second user (the reusable workflow's agent-user: sandbox). By default the reusable workflow, and review.yml, pass --runner-home, which makes the agent the runner's user and refuses session.listen, since the agent could reach the bridge's port ahead of the operator, so no client can attach there.

The user a client logs in as must not be the runner's user. That user holds the job's credentials even after setup removes its root access. SSH as that user would admit a credentialed user, not a chat client. Attaching needs a user of its own that can open the socket and nothing else. The tailnet's rules must also keep one runner from logging in to another.

Also open: several clients at once, a read-only client, and a WebSocket bridge for a browser client, which would have to authenticate the client itself.