Harness SDK

Harness Server

Overview

The server consists of three layers:

Browser ←─ WebSocket ─→ HarnessGateway ─→ HarnessSession ←─ RunnerProtocol ─→ SandboxRunner
              (CF Worker)                  (Durable Object)                    (in container)
  • HarnessGateway — Cloudflare Worker. Edge router that resolves the user/session and forwards to the right Durable Object.
  • HarnessSession — Durable Object. Owns the HarnessState, manages the WebSocket (with hibernation), and orchestrates runs.
  • SandboxRunner — Thin client inside the sandbox container. Receives instructions from the DO, runs claude-code, streams results back.

The RunnerProtocol is the internal protocol between the DO and the sandbox runner.

HarnessGateway

The gateway is a stateless Cloudflare Worker. It handles incoming WebSocket upgrade requests, resolves which HarnessSession DO to route to, and forwards the connection.

GET /ws?userId=abc → upgrade to WebSocket → forward to HarnessSession DO

The gateway does not hold state. It maps a user/session identifier to a Durable Object ID and hands off the connection.

HarnessSession

The Durable Object is the core of the server. It:

  1. Accepts WebSocket connections from the client (via the gateway)
  2. Hibernates when idle — the DO sleeps and the WebSocket is held by the Cloudflare runtime, consuming no compute
  3. Wakes on incoming message — when the client sends a command, the DO wakes up
  4. Owns HarnessState — the single source of truth for messages, status, errors
  5. Translates between protocols — converts HarnessCommands into RunnerProtocol calls and runner events into state deltas

Lifecycle

Client connects
  → DO wakes, sends state snapshot
  → DO hibernates

Client sends submit command
  → DO wakes
  → DO updates state (status: running, add user message)
  → DO sends deltas to client
  → DO starts sandbox runner (if not running)
  → DO forwards prompt via RunnerProtocol
  → Runner streams events back
  → DO translates events into deltas, sends to client
  → Run completes, DO updates state (status: idle)
  → DO hibernates

Client disconnects
  → DO cleans up WebSocket
  → DO hibernates (sandbox may stay warm)

State Management

The DO maintains HarnessState in memory (and optionally persists to SQLite for resume). When state changes, it computes deltas and sends them to connected clients.

// Pseudocode
onCommand(command: HarnessCommand) {
  if (command.type === "submit") {
    this.state.status = "running";
    this.state.messages.push({ id, role: "user", content: command.prompt, status: "complete" });
    this.sendDelta(/* ... */);
    this.startRun(command.prompt);
  }

  if (command.type === "cancel") {
    this.abortCurrentRun();
    this.state.status = "idle";
    this.sendDelta(/* ... */);
  }
}

WebSocket Hibernation

The DO uses Cloudflare's WebSocket Hibernation API. Between messages, the DO consumes zero compute. The runtime holds the WebSocket open and wakes the DO when a message arrives or the connection closes.

Key handlers:

  • webSocketMessage(ws, message) — DO wakes, processes command
  • webSocketClose(ws) — DO wakes, cleans up
  • webSocketError(ws) — DO wakes, handles error

SandboxRunner

A lightweight HTTP server running inside the sandbox container. It exposes a simple API for the DO to call.

The runner is the only process that interacts with claude-code / the agent SDK. It:

  • Receives a prompt
  • Runs the agent
  • Streams events back to the caller

The runner is stateless between runs. It does not know about users, sessions, or WebSockets.

RunnerProtocol

The protocol between HarnessSession (DO) and SandboxRunner (container). Uses HTTP with SSE responses.

Start a Run

POST /query
Content-Type: application/json

{ "prompt": "..." }

Response: SSE stream

event: run.started
data: {"type":"run.started","requestId":"..."}

event: assistant.delta
data: {"type":"assistant.delta","text":"Hello"}

event: tool.started
data: {"type":"tool.started","toolName":"Read","toolUseId":"..."}

event: assistant.delta
data: {"type":"assistant.delta","text":" world"}

event: run.completed
data: {"type":"run.completed","result":"..."}

Health Check

GET /health

Response:

{ "ok": true, "busy": false, "hasAnthropicKey": true }

Event Types

EventFieldsMeaning
run.startedrequestIdRun has begun
assistant.deltatextIncremental text from the assistant
tool.startedtoolName, toolUseIdA tool is being invoked
run.completedresult?, sessionId?Run finished successfully
run.errormessageRun failed

Event → Delta Translation

The HarnessSession translates runner events into deltas for the client:

Runner EventState Delta
run.startedSet status to "running", add user message
assistant.deltaappend-text on current assistant message content
tool.startedAdd tool call to current assistant message
run.completedSet message status to "complete", set status to "idle"
run.errorSet error, set status to "error"

Sandbox Lifecycle

The DO manages the sandbox container via the Cloudflare Containers API:

  1. First request — container starts, runner process is launched inside it
  2. Subsequent requests — container is reused if healthy (health check via /health)
  3. Idle timeout — container is stopped by the platform after inactivity
  4. EnvironmentANTHROPIC_API_KEY and config are injected via setEnvVars before each run