orbotodocs
Agents & AI

Publish an agent session live

How runners stream an agent session to orboto through a harness adapter, how a headless login is handed to an operator, and the relay contract for your own adapter.

Added in v0.210.0 A runner that starts an agent also publishes the agent's session to orboto, so operators can watch and steer it from the browser, the phone or the terminal. This page is for whoever runs agents: which runners publish, how each agent tool (the harness: Claude Code, Codex, Pi, or any terminal program) is adapted, how a login reaches an operator, and the contract a custom adapter speaks.

What publishes on its own

Every session a runner starts registers itself and streams its transcript:

  • orboto pi-runner, local lanes and --remote lanes (see Run an agent fleet),
  • orboto agent-runner with pi, Codex, Claude Code and protocol lanes,
  • orboto codex-runner.

Nothing to configure: the runner uses the credential it already has and connects out over the agent WebSocket, so the machine opens no port. Set ORBOTO_LIVE_SESSION=off to switch the stream off for one runner. An instance that does not offer live sessions refuses the registration; the runner logs this once and keeps working without a stream.

orboto claude, the interactive Claude Code session a person starts, is not runner-owned and publishes nothing by itself. Run it inside tmux and publish the pane with orboto live-session pty (below).

Publish a session yourself

For a session no runner owns, start the stream with orboto live-session:

orboto live-session run --harness claude_code -- claude --name "review"   # Claude Code through its stream-json mode
orboto live-session run --harness codex                                   # codex app-server over stdio
orboto live-session run --harness pi                                      # pi --mode rpc
orboto live-session pty --tmux agent                                      # any program in a tmux pane
orboto live-session login --harness claude_code                           # only a headless login, handed to an operator

run starts the harness and streams it through its adapter. Without a command after --, it starts claude, codex app-server --listen stdio:// or pi --mode rpc; for Claude Code it adds the stream-json flags the adapter needs when your command lacks them. The session's name is the --name of the command, else --label, else the harness and host.

pty streams a tmux pane: its output as text, operator messages and keystrokes typed into it. It pipes the pane into a file under ~/.orboto/live/ unless --log names a file the pane already pipes into (an existing pipe stays). The session ends when the pane is gone.

Common flags for all three:

FlagEffect
--label <text>How operators see the session.
--project <KEY>The project the session works in; decides who may watch it.
--ticket <KEY>The ticket the session works on; sets the project too.
--external-id <id>A stable id: a restarted stream resumes the same session and its transcript.

How each harness is adapted

HarnessTranscriptQuestionsOperator messagesLogin
Claude Code--output-format stream-jsontool approvals (--permission-prompt-tool stdio), AskUserQuestion, usage limits--input-format stream-json; in a lane that runs one turn at a time, a message waits for the next turnheadless browser login, see below
Codexapp-server notificationscommand and file approvals, request_user_input, usage limitssteered into a running turn, otherwise the next turndevice code
Pi--mode rpc eventsextension select, confirm and input prompts, provider limitsa prompt, or a steering message during a turnnone in RPC mode
Any other programthe tmux panenumbered menus after a question line, limit notices, Claude Code and Codex login promptstyped into the pane; raw keystrokesthe login URL printed in the pane

What the Remote console shows for each harness today is listed there.

A session that no adapter covers, such as the Gemini CLI, runs through the terminal fallback (live-session pty). Added in v0.213.0 orboto codex and orboto gemini stream their terminal the same way when you are logged in to orboto, with no transcript; Added in v0.226.2 orboto kimi and orboto agy stream the terminal and a structured transcript; what each launcher provides is on Launchers and the uniform flags.

The login question

An agent on a headless machine cannot open a browser to sign in. The adapter starts the harness's login so that it opens nothing locally, captures the login link, and sends it to the operators as a login question. The operator signs in on their own device and sends back what the harness needs:

  • Claude Code: /login typed into the session (or orboto live-session login --harness claude_code) runs claude auth login with BROWSER=false. Claude Code then prints a login URL that redirects to Anthropic's code page, not to localhost, so it works from a phone. The operator signs in, copies the code the page shows and sends it; the adapter types it into Claude Code's Paste code here if prompted prompt. A login URL that points anywhere else (localhost, an IP address, plain http) is refused.
  • Codex: /login inside an app-server session starts the device-code login (standalone: orboto live-session login --harness codex, which runs codex login --device-auth). The operator gets the URL, the one-time code and its expiry, enters the code on that page and confirms.
  • Pi: no login command in RPC mode; /login is refused with the reason. Give the lane an API key, or sign in inside pi's terminal published with live-session pty.
  • Terminal fallback: a Claude Code or Codex login printed in the pane becomes the same question, and the code is typed into the pane.

A session has at most one open login question: a new login prompt withdraws the older one. The code is masked in every later line.

What the runner masks before sending

Before a line leaves the machine, the runner replaces what it knows is secret: the CLI's own token, environment values whose name reads like a credential (TOKEN, SECRET, PASSWORD, API_KEY and similar, at least eight characters), and every login code an operator sent, also when a terminal wrapped the value across two lines. Every value a tool call types into a web page through the orboto browser (orboto browser type and select, browser_type, browser_fill_form, browser_select_option, orboto_act) becomes ***** in that call and in every later line. The runner cannot know at that moment whether the field was a password, so it masks every typed value. The instance then runs its own secret detector over each stored line.

Delivery guarantees

  • An input the relay delivers twice after a reconnect reaches the harness once; the second delivery gets the first answer.
  • A Pi prompt is confirmed only after Pi accepted it; otherwise it is reported as refused, with Pi's reason or after 30 seconds.
  • Output the runner could not keep up with, or lines lost while the connection was away, show as a status line with the count, and the next line is marked truncated.
  • Switching the agent to another login profile is accepted only by a runner that has login profiles configured, and refused with the reason otherwise.

The relay contract

Write your own adapter against the same contract the built-in ones use. All routes use the agent's normal API key; discover their request and response schemas with orboto api-search "live session" (see the API overview).

Runner side:

  1. POST /agents/live-sessions registers the session with harness (claude_code, codex, pi, gemini, pty or other), label, host and optionally projectKey or ticketKey, laneId, model, externalId (the same id and account resume the session and its numbering) and inputKinds (what the adapter accepts: text, answer, keystroke, switch_account, Added in v0.230.0 set_limit_policy). Send the x-orboto-agent-session header of your process. The answer carries the session id and, once, a relay token.
  2. Get a socket ticket with POST /auth/ws-ticket, open /agent/ws?ticket=... and send live_attach {sessionId, token}. orboto answers live_attached {lastSeq}, the current takeover, and every input still waiting.
  3. Send one live_event {sessionId, seq, event} per transcript line, seq counting up from 1; after a reconnect resend everything after lastSeq (repeated numbers are ignored). Event kinds: text, tool_call, tool_result, question, permission_prompt, limit_prompt, login (an https url, optional deviceCode and expiresAt) and status. A line may mark secret ranges of its text with secretSpans. One frame stays under 60 KiB.
  4. Answer every live_input {inputId, input, actor} with live_input_ack {inputId, status} (delivered, rejected or unsupported, optional reason). Report a question answered at the terminal or withdrawn with live_question_resolved.
  5. PATCH /agents/live-sessions/:id/heartbeat every 30 seconds (two minutes without one read as lost) and PATCH /agents/live-sessions/:id/ended at the end: the takeover ends, open questions are withdrawn and waiting inputs expire.

Added in v0.230.0 The socket ticket keeps the scopes of the API key that requested it, so a runner on a scoped key follows tickets, comments and live sessions inside those scopes. When the key is revoked, rotated, rescoped or given a new expiry date, when it expires, or when its account is deactivated or deleted, the socket closes with code 4401. Request a new ticket with the key, attach again with live_attach and resend everything after lastSeq. A 401 on the ticket request means the key or the account is gone: stop and mint a new key. See Live connections.

Operator side:

RoutePurpose
GET /agents/live-sessionsSessions you may see, newest first, cursor-paginated (state, projectId, laneId filters)
GET /agents/live-sessions/:idOne session with its open questions and takeover
GET /agents/live-sessions/:id/transcriptTranscript pages (cursor, order, afterSeq)
POST /agents/live-sessions/:id/questions/:questionId/answerAnswer with optionIds and/or text
POST /agents/live-sessions/:id/inputA message (text), keystrokes (keystroke), switch_account or Added in v0.230.0 set_limit_policy
POST /agents/live-sessions/:id/leaseTake over, or renew your takeover
POST /agents/live-sessions/:id/lease/requestAsk the holder to hand over
DELETE /agents/live-sessions/:id/leaseGive back
POST /agents/live-sessions/:id/lease/revokeEnd someone else's takeover (admin:agents:write)

Added in v0.230.0 A runner that rotates login profiles can publish what it does at a usage limit: limitPolicy on the registration and on a heartbeat (mode off, ask, stop or switch, everyProfile, profiles, threshold, strategy, resume, active, source, changedAt, changedBy), which GET /agents/live-sessions/:id returns as the session's current policy. It lists set_limit_policy in inputKinds and takes {"kind":"set_limit_policy","policy":{"mode":"switch","profiles":["work","second"],"threshold":85}} to change the policy without a restart; a switch order may name only profiles the session published (400 otherwise), threshold is 1 to 100, and resume belongs to ask and stop. The input follows the permission rules and the takeover of switch_account, is audited as agent.live.input with the new and the previous policy, and the runner answers it with live_input_ack as it does every input. The full description is in Switching on and off.

A second operator who tries to take over gets 409 naming the holder. A terminal client can follow a session by subscribing to orboto://live-session/<id> on the agent WebSocket; it is woken on every change and reads the new lines with afterSeq. There are no named MCP tools for live sessions; MCP clients reach every route through orboto_api_search and orboto_api_call.

On this page