Skip to content

Worker Interaction System

The Worker Interaction System enables real-time interaction between users and running AI workers. It consists of three components: a sidecar inside the sandbox, a renderer on the user’s terminal, and a control channel that bridges them through the control plane.

USER'S MACHINE CONTROL PLANE SANDBOX
+-----------------------+ +------------------+ +--------------------+
| | | | | |
| arpi attach w1 |<-- WS --->| SDK event | | agent (--print) |
| +-------------------+ | events | store (PG) | | | |
| | bubbletea TUI | | +ctrl | + NATS pub/sub |<- HTTP -| arpi-sidecar |
| | (renderer) | | | + control API | | - parse stdout |
| +-------------------+ | | | | - emit events |
| | +------------------+ | - relay control |
+-----------------------+ | - monitor proc |
+--------------------+
  1. Renderer connects to WebSocket event stream (/v1/workers/{id}/sdk-events/stream)
  2. SDK events render as TUI widgets: streaming text, tool cards, collapsible results, progress bar
  3. When the agent needs tool approval, a ControlRequest flows to the renderer as a modal prompt
  4. User approves/denies via POST /v1/workers/{id}/control
  5. Sidecar polls GET /v1/workers/{id}/control/pending and relays the decision to agent stdin
  6. Detach with ctrl+d — worker keeps running
  1. Sidecar runs with autonomous: true in its config
  2. PermissionEngine auto-approves tools per the auto_approve list
  3. SDK events flow to the store for observability
  4. User reviews later: arpi attach --read-only w1 (replay from cursor 0)

Go binary deployed inside the sandbox at spawn time. Acts as the process manager for the agent.

Location: server/cmd/arpi-sidecar/

Responsibilities:

  • Launch agent subprocess with adapter-specific args
  • Parse agent stdout line-by-line through a pluggable Adapter
  • Emit normalized SDK events via POST /v1/workers/{id}/sdk-events
  • Monitor agent process lifecycle (heartbeat, exit detection)
  • Poll for control responses and relay to agent stdin
  • Apply permission decisions (auto-approve, deny, escalate to human)

Adapter interface:

type Adapter interface {
ParseLine(line []byte) ([]*SDKEvent, error)
FormatControlResponse(resp ControlResponse) []byte
LaunchArgs(agentCmd string) []string
}

Two built-in adapters:

  • claude-code — Parses Claude Code’s --print --output-format stream-json NDJSON output. Maps assistant, tool_use, tool_result, result message types to SDK events.
  • generic — Wraps each stdout line as a SystemEvent. Works with any agent.

Config (deployed as /etc/arpi/sidecar.json):

{
"adapter": "claude-code",
"worker_id": "abc-123",
"control_plane_url": "https://api.arpi.sh",
"auth_token": "...",
"autonomous": false,
"permissions": {
"auto_approve": ["Read", "Glob", "Grep"],
"deny": [],
"ask": ["Write", "Edit", "Bash"]
},
"poll_interval_ms": 500
}

Deployment: The sidecar binary is cross-compiled and embedded in the arpid server binary via //go:embed. During spawn, deploySidecar uploads the binary and config to the sandbox after the credential proxy is deployed (Step 6.5 in the spawn pipeline).

Integrated into the arpi CLI binary. Launched by arpi attach <worker-id>.

Location: cli/internal/renderer/

SDK event rendering:

Event TypeWidget
assistantStreaming text block
tool_useBordered card: [tool_name] input_summary
tool_resultFirst 3 lines of output, error indicator
resultFinal status with icon
progressStatus bar (tokens, cost)
systemLevel-colored dimmed line
control_requestModal prompt: Allow [tool]? (y/n)

Key bindings:

  • ctrl+d / ctrl+c — detach (worker keeps running)
  • y / n — approve/deny pending tool approval
  • up / down / pgup / pgdown — scroll event history
  • q — quit (when no prompt active)

Flags:

  • --read-only — replay events without control channel (auto-set for completed/stopped/failed workers)
  • --raw — raw PTY proxy (not yet implemented, pending OpenSandbox PTY support)

Bidirectional flow using the existing SDK event infrastructure.

Request flow (agent needs approval):

agent stdout → sidecar adapter → POST /v1/workers/{id}/sdk-events (type: control_request)
→ sdk.Store.Insert() → WebSocket stream → renderer shows modal prompt

Response flow (user decides):

user presses y/n → POST /v1/workers/{id}/control
→ sdk.Store.Insert(control_response) → NATS publish
→ sidecar polls GET /v1/workers/{id}/control/pending?after_id={cursor}
→ adapter.FormatControlResponse() → agent stdin

The control channel reuses the sdk_events Postgres table — control_request and control_response are SDK event types. Pending responses use cursor-based pagination (after_id parameter) to prevent re-delivery.

Templates opt into the sidecar via a [sidecar] TOML section:

[sidecar]
adapter = "claude-code" # "claude-code" | "codex" | "generic"
auto_approve = ["Read", "Glob", "Grep", "LS"]
deny = []
ask = ["Write", "Edit", "Bash"]

Templates without a [sidecar] section skip sidecar deployment entirely. The permission engine uses three lists:

  • auto_approve — tools silently allowed
  • deny — tools silently rejected
  • ask — tools requiring human approval (auto-approved in autonomous mode)

Tools not in any list default to “ask” (or “allow” in autonomous mode).

MethodPathDescription
POST/v1/workers/{id}/sdk-eventsReport SDK event (sidecar → server)
GET/v1/workers/{id}/sdk-eventsList events (cursor-based)
WS/v1/workers/{id}/sdk-events/streamStream events (WebSocket, polling)
MethodPathDescription
POST/v1/workers/{id}/controlSubmit control response (renderer → server)
GET/v1/workers/{id}/control/pendingPoll pending responses (server → sidecar)

Eight normalized event types, defined in proto/arpi/v1/sdk_event.proto:

TypeDirectionDescription
assistantagent → userText output from the agent
tool_useagent → userAgent invoking a tool
tool_resultagent → userTool execution result
resultagent → userAgent completed its turn
progressagent → userToken/cost counters
systemagent → userSystem-level notification
control_requestagent → userPermission needed for a tool
control_responseuser → agentPermission decision

The sidecar is deployed as Step 6.5 in the spawn workflow, between the credential proxy (Step 6) and finalize (Step 7):

  1. Select embedded binary for target arch (linux/amd64)
  2. Upload to /usr/local/bin/arpi-sidecar
  3. Read auth token from /var/run/arpi/token (written by cred-proxy step)
  4. Build config from template [sidecar] section
  5. Upload config to /etc/arpi/sidecar.json
  6. Start sidecar in background: ARPI_SIDECAR_CONFIG=/etc/arpi/sidecar.json arpi-sidecar

The sidecar then launches the agent as a subprocess and manages its lifecycle.

  • Sidecar auth: Uses the same per-worker bearer token as the credential proxy. Token is generated during spawn and stored at /var/run/arpi/token.
  • No NATS from sandbox: The sidecar communicates with the control plane exclusively via HTTP. NATS access is not exposed to the sandbox network, maintaining the two-wall security boundary.
  • Config via Upload: Sidecar config is written via the compute Upload API, not shell interpolation, preventing injection attacks.
  • Ownership validation: Control endpoints validate worker existence before accepting requests.
  • server/cmd/arpi-sidecar/ — sidecar binary (8 files)
  • server/internal/sidecar/types/types.go — shared wire types
  • server/internal/sdk/control.go — control query methods
  • server/internal/api/control.go — control HTTP endpoints
  • server/internal/api/sdk_events.go — SDK event HTTP/WS endpoints (modified)
  • server/internal/spawn/deploy_sidecar.go — spawn pipeline step
  • server/internal/spawn/sidecar_embed.go — embedded binary
  • cli/internal/renderer/ — bubbletea TUI (model, widgets)
  • cli/internal/api/events.go — WebSocket + control client
  • cli/cmd/attach.go — cobra command
  • proto/arpi/v1/sdk_event.proto — SDK event type definitions