From 633aa2a52b2f1d471ab651315553248d647e9113 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Sat, 16 May 2026 09:04:19 -0400 Subject: [PATCH 1/2] docs: broker HTTP / WS API reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The broker's listen API (port 3888 by default) is what the dashboard, the CLI, the SDKs, and any custom integration call to spawn agents, inject PTY input, subscribe to events, and shut the broker down. None of that surface was documented anywhere outside the source. Adds `reference-broker-api` covering: - base URL / port / `--api-bind` flag - auth model: `X-API-Key` or `Bearer`, `RELAY_BROKER_API_KEY` env, unauthenticated fallback when unset, `/health` exempt - all 22 routes grouped as Health/Config, Agent lifecycle, PTY interaction, Event stream — with body shapes for the ones a caller would actually compose by hand (`/api/spawn`, `/api/send`, `/api/input/{name}`) - `/ws` protocol: ping cadence, `?sinceSeq=` resume, `replay_gap` frame, full inventory of durable + ephemeral event `kind`s - worked end-to-end curl + websocat example to spawn, observe, drive, message, and release an agent - error envelope and common codes Mirrors to `docs/reference-broker-api.md` per `.claude/rules/docs-sync.md`. Adds the slug to the CLI nav group in `web/lib/docs-nav.ts`. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/reference-broker-api.md | 246 +++++++++++++++++++++ web/content/docs/reference-broker-api.mdx | 247 ++++++++++++++++++++++ web/lib/docs-nav.ts | 1 + 3 files changed, 494 insertions(+) create mode 100644 docs/reference-broker-api.md create mode 100644 web/content/docs/reference-broker-api.mdx diff --git a/docs/reference-broker-api.md b/docs/reference-broker-api.md new file mode 100644 index 000000000..ba4bc84ac --- /dev/null +++ b/docs/reference-broker-api.md @@ -0,0 +1,246 @@ +# Broker HTTP / WS API + +Reference for the listen API that the broker exposes for dashboards, the CLI, and custom integrations. + +The broker daemon (`agent-relay-broker`) exposes an HTTP + WebSocket +listen API once it's running. This is the surface that the dashboard, +the `agent-relay` CLI, the SDKs, and any custom integration use to +spawn agents, inject input into PTYs, observe live events, and +shut the broker down. + +## Base URL and port + +The listen API binds to the port you pass to `agent-relay up` (default +**3888**), on `127.0.0.1` by default. Bind to a non-loopback address +with `--api-bind` if you need remote access: + +```bash +agent-relay up --port 3888 # local only +agent-relay up --port 3888 --api-bind 0.0.0.0 # accept remote +``` + +All routes below live under `http://:`. + +## Authentication + +The broker requires an API key on every protected route. Pass it as +either header: + +``` +X-API-Key: +Authorization: Bearer +``` + +The expected token is read from the `RELAY_BROKER_API_KEY` environment +variable. If that variable is unset, the broker runs **unauthenticated** +— protected routes accept any request. In production, always set it. + +The only route exempt from auth is `GET /health`. + +## Routes + +### Health and configuration + +| Method | Path | Purpose | +| ------ | --------------------- | ----------------------------------------------------------------- | +| `GET` | `/health` | Liveness probe + workspace/startup status. Unauthenticated. | +| `GET` | `/api/session` | Broker version, protocol version, persist/ephemeral mode, uptime. | +| `POST` | `/api/session/renew` | Renew the broker lease (persist mode only). | +| `GET` | `/api/config` | Relaycast workspace key and workspace memberships. | +| `GET` | `/api/metrics` | Broker metrics. Optional `?agent=` filter. | +| `GET` | `/api/status` | Aggregate broker status. | +| `GET` | `/api/crash-insights` | Recent crash diagnostics. | +| `GET` | `/api/history/stats` | Stub message-history counters. | +| `POST` | `/api/preflight` | Check that each `{ name, cli }` agent can be spawned. | +| `POST` | `/api/shutdown` | Graceful broker shutdown. | + +### Agent lifecycle + +| Method | Path | Purpose | +| -------- | -------------------------------------- | ------------------------------------------------------------------------- | +| `POST` | `/api/spawn` | Spawn an agent as a child of the broker. | +| `GET` | `/api/spawned` | List running agents. | +| `DELETE` | `/api/spawned/{name}` | Release / kill an agent. Optional body `{ "reason": "..." }`. | +| `POST` | `/api/spawned/{name}/model` | Change an agent's model. Body `{ "model": "...", "timeoutMs"?: number }`. | +| `POST` | `/api/spawned/{name}/subscribe` | Subscribe an agent to channels. Body `{ "channels": ["..."] }`. | +| `POST` | `/api/spawned/{name}/unsubscribe` | Unsubscribe an agent from channels. Body `{ "channels": ["..."] }`. | +| `POST` | `/api/agents/by-name/{name}/interrupt` | Interrupt an agent (not yet implemented — returns 501). | + +#### `POST /api/spawn` + +Body (fields accept both camelCase and snake_case): + +```json +{ + "name": "Alice", + "cli": "claude", + "model": "sonnet", + "args": ["--no-color"], + "task": "Read the README and summarize it", + "channels": ["#general"], + "cwd": "/Users/me/project", + "team": "demo", + "shadowOf": null, + "shadowMode": null, + "continueFrom": null, + "idleThresholdSecs": 0, + "skipRelayPrompt": false, + "restartPolicy": null, + "agentToken": null +} +``` + +`name` and `cli` are required. Returns `{ "success": true, ... }` on +success or `{ "success": false, "error": "..." }` with a non-2xx +status on failure. + +### PTY interaction + +| Method | Path | Purpose | +| ------ | -------------------- | ----------------------------------------------------------------- | +| `POST` | `/api/input/{name}` | Send raw bytes to an agent's PTY stdin. Body `{ "data": "..." }`. | +| `POST` | `/api/resize/{name}` | Resize an agent's PTY. Body `{ "rows": , "cols": }`. | +| `POST` | `/api/send` | Inject a relay message into an agent. | + +#### `POST /api/input/{name}` + +This is the keystroke channel. The `data` string is written to the +target agent's stdin verbatim — escape characters are passed through. + +```bash +curl -X POST localhost:3888/api/input/Alice \ + -H "X-API-Key: $RELAY_BROKER_API_KEY" \ + -d '{"data":"hello\n"}' +``` + +Returns 404 if the agent isn't found. + +#### `POST /api/send` + +```json +{ + "to": "Alice", + "message": "Please review PR #837", + "from": "Bob", + "thread": "thr_abc", + "workspaceId": "ws_demo", + "mode": "wait" +} +``` + +The message text field accepts any of `message`, `text`, `body`, or +`content`. The `mode` field accepts `wait` (default — queue and +inject when the agent is idle) or `steer` (inject immediately, +even mid-response). Returns 504 on a 30s broker timeout; 404 if +the target agent isn't registered. + +### Event stream + +| Method | Path | Purpose | +| ------ | -------------------- | ---------------------------------------------------- | +| `GET` | `/ws` | WebSocket: subscribe to broker events. | +| `GET` | `/api/events/replay` | HTTP-based replay of events since a sequence number. | + +#### `GET /ws` + +Upgrade to WebSocket and you'll receive every broker event as a JSON +text frame. The broker pings every 30s; respond with pong to stay +connected. + +To resume after a disconnect without missing durable events, include +the last sequence number you saw: + +``` +ws://localhost:3888/ws?sinceSeq=12345 +``` + +If the requested sequence is older than the replay buffer's window, +the first frame you receive will be: + +```json +{ "kind": "replay_gap", "requestedSinceSeq": 12345, "oldestAvailable": 14000, "seq": 14999 } +``` + +> **Note:** Two event kinds are **ephemeral** and never stored in the +> replay buffer: `worker_stream` (PTY output chunks — high frequency) +> and `delivery_active` (in-flight delivery progress). If you +> disconnect, you cannot replay them. + +#### Event kinds emitted on `/ws` + +Durable (replayable via `?sinceSeq=...`): + +| `kind` | When it fires | +| -------------------------------------- | ------------------------------------------------------ | +| `agent_spawned` | An agent was successfully spawned. | +| `agent_exit` / `agent_exited` | Worker process exited. | +| `agent_idle` | Worker has been quiet past `idleThresholdSecs`. | +| `agent_restarting` / `agent_restarted` | Worker is being restarted under the restart policy. | +| `agent_released` | Worker was released by `/api/spawned/{name}` DELETE. | +| `agent_permanently_dead` | Worker exhausted restart attempts. | +| `worker_ready` | Worker finished startup and is ready for input. | +| `worker_error` | Worker emitted an error frame. | +| `relay_inbound` | Inbound relay message routed to an agent. | +| `delivery_ack` | Message delivery acknowledged. | +| `delivery_verified` | Echo verification confirmed the message was delivered. | +| `delivery_failed` | Message delivery failed. | +| `delivery_dropped` | Delivery was dropped (e.g. agent gone). | +| `delivery_retry` | Delivery is being retried. | + +Ephemeral (broadcast only, no replay): + +| `kind` | When it fires | +| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `worker_stream` | A chunk of stdout from a wrapped CLI. Payload contains `stream` (`"stdout"`) and `chunk` (the raw bytes — typically still ANSI-escaped). | +| `delivery_active` | High-frequency progress events for in-flight deliveries. | + +## Worked example: control a spawned agent end-to-end + +```bash +KEY="$RELAY_BROKER_API_KEY" + +# 1. Start the broker +agent-relay up --port 3888 & + +# 2. Spawn Alice running claude +curl -sX POST localhost:3888/api/spawn \ + -H "X-API-Key: $KEY" \ + -d '{"name":"Alice","cli":"claude"}' + +# 3. Stream her PTY output (filter for her worker_stream frames) +websocat ws://localhost:3888/ws \ + -H "X-API-Key: $KEY" \ + | jq -r 'select(.kind=="worker_stream" and .name=="Alice") | .chunk' + +# 4. Send a keystroke to Alice's CLI +curl -sX POST localhost:3888/api/input/Alice \ + -H "X-API-Key: $KEY" \ + -d '{"data":"hello\n"}' + +# 5. Inject a relay message +curl -sX POST localhost:3888/api/send \ + -H "X-API-Key: $KEY" \ + -d '{"to":"Alice","from":"Bob","message":"please review #837"}' + +# 6. Release her +curl -sX DELETE localhost:3888/api/spawned/Alice \ + -H "X-API-Key: $KEY" +``` + +## Error envelope + +Failed responses return a consistent envelope: + +```json +{ + "error": { + "code": "agent_not_found", + "message": "no agent named Alice", + "statusCode": 404 + } +} +``` + +Common codes: `agent_not_found` (404), `invalid_request` (400), +`unsupported_operation` (400), `unauthorized` (401), `request_failed` +(400), `internal_error` (500). diff --git a/web/content/docs/reference-broker-api.mdx b/web/content/docs/reference-broker-api.mdx new file mode 100644 index 000000000..5af68886b --- /dev/null +++ b/web/content/docs/reference-broker-api.mdx @@ -0,0 +1,247 @@ +--- +title: 'Broker HTTP / WS API' +description: 'Reference for the listen API that the broker exposes for dashboards, the CLI, and custom integrations.' +--- + +The broker daemon (`agent-relay-broker`) exposes an HTTP + WebSocket +listen API once it's running. This is the surface that the dashboard, +the `agent-relay` CLI, the SDKs, and any custom integration use to +spawn agents, inject input into PTYs, observe live events, and +shut the broker down. + +## Base URL and port + +The listen API binds to the port you pass to `agent-relay up` (default +**3888**), on `127.0.0.1` by default. Bind to a non-loopback address +with `--api-bind` if you need remote access: + +```bash +agent-relay up --port 3888 # local only +agent-relay up --port 3888 --api-bind 0.0.0.0 # accept remote +``` + +All routes below live under `http://:`. + +## Authentication + +The broker requires an API key on every protected route. Pass it as +either header: + +``` +X-API-Key: +Authorization: Bearer +``` + +The expected token is read from the `RELAY_BROKER_API_KEY` environment +variable. If that variable is unset, the broker runs **unauthenticated** +— protected routes accept any request. In production, always set it. + +The only route exempt from auth is `GET /health`. + +## Routes + +### Health and configuration + +| Method | Path | Purpose | +| ------ | ---- | ------- | +| `GET` | `/health` | Liveness probe + workspace/startup status. Unauthenticated. | +| `GET` | `/api/session` | Broker version, protocol version, persist/ephemeral mode, uptime. | +| `POST` | `/api/session/renew` | Renew the broker lease (persist mode only). | +| `GET` | `/api/config` | Relaycast workspace key and workspace memberships. | +| `GET` | `/api/metrics` | Broker metrics. Optional `?agent=` filter. | +| `GET` | `/api/status` | Aggregate broker status. | +| `GET` | `/api/crash-insights` | Recent crash diagnostics. | +| `GET` | `/api/history/stats` | Stub message-history counters. | +| `POST` | `/api/preflight` | Check that each `{ name, cli }` agent can be spawned. | +| `POST` | `/api/shutdown` | Graceful broker shutdown. | + +### Agent lifecycle + +| Method | Path | Purpose | +| ------ | ---- | ------- | +| `POST` | `/api/spawn` | Spawn an agent as a child of the broker. | +| `GET` | `/api/spawned` | List running agents. | +| `DELETE` | `/api/spawned/{name}` | Release / kill an agent. Optional body `{ "reason": "..." }`. | +| `POST` | `/api/spawned/{name}/model` | Change an agent's model. Body `{ "model": "...", "timeoutMs"?: number }`. | +| `POST` | `/api/spawned/{name}/subscribe` | Subscribe an agent to channels. Body `{ "channels": ["..."] }`. | +| `POST` | `/api/spawned/{name}/unsubscribe` | Unsubscribe an agent from channels. Body `{ "channels": ["..."] }`. | +| `POST` | `/api/agents/by-name/{name}/interrupt` | Interrupt an agent (not yet implemented — returns 501). | + +#### `POST /api/spawn` + +Body (fields accept both camelCase and snake_case): + +```json +{ + "name": "Alice", + "cli": "claude", + "model": "sonnet", + "args": ["--no-color"], + "task": "Read the README and summarize it", + "channels": ["#general"], + "cwd": "/Users/me/project", + "team": "demo", + "shadowOf": null, + "shadowMode": null, + "continueFrom": null, + "idleThresholdSecs": 0, + "skipRelayPrompt": false, + "restartPolicy": null, + "agentToken": null +} +``` + +`name` and `cli` are required. Returns `{ "success": true, ... }` on +success or `{ "success": false, "error": "..." }` with a non-2xx +status on failure. + +### PTY interaction + +| Method | Path | Purpose | +| ------ | ---- | ------- | +| `POST` | `/api/input/{name}` | Send raw bytes to an agent's PTY stdin. Body `{ "data": "..." }`. | +| `POST` | `/api/resize/{name}` | Resize an agent's PTY. Body `{ "rows": , "cols": }`. | +| `POST` | `/api/send` | Inject a relay message into an agent. | + +#### `POST /api/input/{name}` + +This is the keystroke channel. The `data` string is written to the +target agent's stdin verbatim — escape characters are passed through. + +```bash +curl -X POST localhost:3888/api/input/Alice \ + -H "X-API-Key: $RELAY_BROKER_API_KEY" \ + -d '{"data":"hello\n"}' +``` + +Returns 404 if the agent isn't found. + +#### `POST /api/send` + +```json +{ + "to": "Alice", + "message": "Please review PR #837", + "from": "Bob", + "thread": "thr_abc", + "workspaceId": "ws_demo", + "mode": "wait" +} +``` + +The message text field accepts any of `message`, `text`, `body`, or +`content`. The `mode` field accepts `wait` (default — queue and +inject when the agent is idle) or `steer` (inject immediately, +even mid-response). Returns 504 on a 30s broker timeout; 404 if +the target agent isn't registered. + +### Event stream + +| Method | Path | Purpose | +| ------ | ---- | ------- | +| `GET` | `/ws` | WebSocket: subscribe to broker events. | +| `GET` | `/api/events/replay` | HTTP-based replay of events since a sequence number. | + +#### `GET /ws` + +Upgrade to WebSocket and you'll receive every broker event as a JSON +text frame. The broker pings every 30s; respond with pong to stay +connected. + +To resume after a disconnect without missing durable events, include +the last sequence number you saw: + +``` +ws://localhost:3888/ws?sinceSeq=12345 +``` + +If the requested sequence is older than the replay buffer's window, +the first frame you receive will be: + +```json +{ "kind": "replay_gap", "requestedSinceSeq": 12345, "oldestAvailable": 14000, "seq": 14999 } +``` + +> **Note:** Two event kinds are **ephemeral** and never stored in the +> replay buffer: `worker_stream` (PTY output chunks — high frequency) +> and `delivery_active` (in-flight delivery progress). If you +> disconnect, you cannot replay them. + +#### Event kinds emitted on `/ws` + +Durable (replayable via `?sinceSeq=...`): + +| `kind` | When it fires | +| ------ | ------------- | +| `agent_spawned` | An agent was successfully spawned. | +| `agent_exit` / `agent_exited` | Worker process exited. | +| `agent_idle` | Worker has been quiet past `idleThresholdSecs`. | +| `agent_restarting` / `agent_restarted` | Worker is being restarted under the restart policy. | +| `agent_released` | Worker was released by `/api/spawned/{name}` DELETE. | +| `agent_permanently_dead` | Worker exhausted restart attempts. | +| `worker_ready` | Worker finished startup and is ready for input. | +| `worker_error` | Worker emitted an error frame. | +| `relay_inbound` | Inbound relay message routed to an agent. | +| `delivery_ack` | Message delivery acknowledged. | +| `delivery_verified` | Echo verification confirmed the message was delivered. | +| `delivery_failed` | Message delivery failed. | +| `delivery_dropped` | Delivery was dropped (e.g. agent gone). | +| `delivery_retry` | Delivery is being retried. | + +Ephemeral (broadcast only, no replay): + +| `kind` | When it fires | +| ------ | ------------- | +| `worker_stream` | A chunk of stdout from a wrapped CLI. Payload contains `stream` (`"stdout"`) and `chunk` (the raw bytes — typically still ANSI-escaped). | +| `delivery_active` | High-frequency progress events for in-flight deliveries. | + +## Worked example: control a spawned agent end-to-end + +```bash +KEY="$RELAY_BROKER_API_KEY" + +# 1. Start the broker +agent-relay up --port 3888 & + +# 2. Spawn Alice running claude +curl -sX POST localhost:3888/api/spawn \ + -H "X-API-Key: $KEY" \ + -d '{"name":"Alice","cli":"claude"}' + +# 3. Stream her PTY output (filter for her worker_stream frames) +websocat ws://localhost:3888/ws \ + -H "X-API-Key: $KEY" \ + | jq -r 'select(.kind=="worker_stream" and .name=="Alice") | .chunk' + +# 4. Send a keystroke to Alice's CLI +curl -sX POST localhost:3888/api/input/Alice \ + -H "X-API-Key: $KEY" \ + -d '{"data":"hello\n"}' + +# 5. Inject a relay message +curl -sX POST localhost:3888/api/send \ + -H "X-API-Key: $KEY" \ + -d '{"to":"Alice","from":"Bob","message":"please review #837"}' + +# 6. Release her +curl -sX DELETE localhost:3888/api/spawned/Alice \ + -H "X-API-Key: $KEY" +``` + +## Error envelope + +Failed responses return a consistent envelope: + +```json +{ + "error": { + "code": "agent_not_found", + "message": "no agent named Alice", + "statusCode": 404 + } +} +``` + +Common codes: `agent_not_found` (404), `invalid_request` (400), +`unsupported_operation` (400), `unauthorized` (401), `request_failed` +(400), `internal_error` (500). diff --git a/web/lib/docs-nav.ts b/web/lib/docs-nav.ts index 1dcc38027..43a1eb988 100644 --- a/web/lib/docs-nav.ts +++ b/web/lib/docs-nav.ts @@ -58,6 +58,7 @@ export const docsNav: NavGroup[] = [ { title: 'Cloud commands', slug: 'cli-cloud-commands' }, { title: 'On the relay', slug: 'cli-on-the-relay' }, { title: 'CLI reference', slug: 'reference-cli' }, + { title: 'Broker HTTP / WS API', slug: 'reference-broker-api' }, ], }, { From 389630924cc44d3daac71d5a3e029bd5856c143a Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Sun, 17 May 2026 11:44:42 -0400 Subject: [PATCH 2/2] docs: web/content/docs is canonical; drop stale /docs mirror MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `docs/*.md` is a partial mirror that's gone stale long ago (5 pages there vs 40+ in `web/content/docs/`) and is no longer maintained. The Next.js site under `web/` reads `web/content/docs/*.mdx` directly — that's the source of truth. This PR followed the old `.claude/rules/docs-sync.md` rule and created a `docs/reference-broker-api.md` mirror. Dropping it here along with rewriting the rule so future doc PRs aren't sent down the same wrong path. The canonical reference at `web/content/docs/reference-broker-api.mdx` is untouched. Cleaning up the existing legacy pages in `docs/` is a separate task. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/rules/docs-sync.md | 51 +++++--- docs/reference-broker-api.md | 246 ----------------------------------- 2 files changed, 30 insertions(+), 267 deletions(-) delete mode 100644 docs/reference-broker-api.md diff --git a/.claude/rules/docs-sync.md b/.claude/rules/docs-sync.md index 85f3b435a..b7423b243 100644 --- a/.claude/rules/docs-sync.md +++ b/.claude/rules/docs-sync.md @@ -1,21 +1,30 @@ -# Documentation Sync Rule - -The docs exist in two locations that **must stay in sync**: - -- `web/content/docs/*.mdx` — MDX source (used by the Next.js web app) -- `docs/*.md` — Plain markdown mirror (for LLMs, CLI users, GitHub readers) - -## Rules - -1. **Any change to an `.mdx` file must be mirrored to the corresponding `.md` file**, and vice versa. -2. The markdown files should have the same content but with MDX components converted to plain markdown: - - `` / `` → remove (just keep the code blocks) - - `` → `> **Note:**` - - `` → `> **Warning:**` - - `` / `` → use headers or separate code blocks - - Frontmatter (`---` YAML block) → remove from `.md` files -3. **File mapping** (flat structure, no subdirectories): - - `web/content/docs/{slug}.mdx` ↔ `docs/{slug}.md` - - e.g. `web/content/docs/reference-sdk.mdx` ↔ `docs/reference-sdk.md` -4. If you add a new `.mdx` doc, create the corresponding `.md` mirror. -5. If you update default values, API signatures, or examples — update **both** files. +# Documentation Rule + +## Public docs live in `web/content/docs/*.mdx` + +This is the single source of truth — the Next.js web app under +`web/` reads this directory directly to build the published docs +site. New pages go here, period. + +When you add a new page: + +- Create `web/content/docs/{slug}.mdx` with the standard frontmatter + (`title:` and `description:`). +- Add an entry to the navigation in `web/lib/docs-nav.ts` under the + appropriate group, OR to the `ALL_SLUGS` "hidden but routable" + list if it shouldn't appear in the sidebar. + +That's it. Don't create or update files in the top-level `docs/` +directory. + +## The top-level `docs/` directory is legacy + +It contains a partial mirror of a handful of MDX pages converted to +plain markdown. It was originally maintained as an "LLMs / CLI users +/ GitHub readers" alternate, but it drifted out of sync long ago +(5 pages survive vs 40+ in `web/content/docs/`) and is no longer +authoritative. Do not add new files to it, and do not "mirror" your +MDX changes into it. + +Existing pages there will be cleaned up separately. Treat the +directory as read-only legacy until that happens. diff --git a/docs/reference-broker-api.md b/docs/reference-broker-api.md deleted file mode 100644 index ba4bc84ac..000000000 --- a/docs/reference-broker-api.md +++ /dev/null @@ -1,246 +0,0 @@ -# Broker HTTP / WS API - -Reference for the listen API that the broker exposes for dashboards, the CLI, and custom integrations. - -The broker daemon (`agent-relay-broker`) exposes an HTTP + WebSocket -listen API once it's running. This is the surface that the dashboard, -the `agent-relay` CLI, the SDKs, and any custom integration use to -spawn agents, inject input into PTYs, observe live events, and -shut the broker down. - -## Base URL and port - -The listen API binds to the port you pass to `agent-relay up` (default -**3888**), on `127.0.0.1` by default. Bind to a non-loopback address -with `--api-bind` if you need remote access: - -```bash -agent-relay up --port 3888 # local only -agent-relay up --port 3888 --api-bind 0.0.0.0 # accept remote -``` - -All routes below live under `http://:`. - -## Authentication - -The broker requires an API key on every protected route. Pass it as -either header: - -``` -X-API-Key: -Authorization: Bearer -``` - -The expected token is read from the `RELAY_BROKER_API_KEY` environment -variable. If that variable is unset, the broker runs **unauthenticated** -— protected routes accept any request. In production, always set it. - -The only route exempt from auth is `GET /health`. - -## Routes - -### Health and configuration - -| Method | Path | Purpose | -| ------ | --------------------- | ----------------------------------------------------------------- | -| `GET` | `/health` | Liveness probe + workspace/startup status. Unauthenticated. | -| `GET` | `/api/session` | Broker version, protocol version, persist/ephemeral mode, uptime. | -| `POST` | `/api/session/renew` | Renew the broker lease (persist mode only). | -| `GET` | `/api/config` | Relaycast workspace key and workspace memberships. | -| `GET` | `/api/metrics` | Broker metrics. Optional `?agent=` filter. | -| `GET` | `/api/status` | Aggregate broker status. | -| `GET` | `/api/crash-insights` | Recent crash diagnostics. | -| `GET` | `/api/history/stats` | Stub message-history counters. | -| `POST` | `/api/preflight` | Check that each `{ name, cli }` agent can be spawned. | -| `POST` | `/api/shutdown` | Graceful broker shutdown. | - -### Agent lifecycle - -| Method | Path | Purpose | -| -------- | -------------------------------------- | ------------------------------------------------------------------------- | -| `POST` | `/api/spawn` | Spawn an agent as a child of the broker. | -| `GET` | `/api/spawned` | List running agents. | -| `DELETE` | `/api/spawned/{name}` | Release / kill an agent. Optional body `{ "reason": "..." }`. | -| `POST` | `/api/spawned/{name}/model` | Change an agent's model. Body `{ "model": "...", "timeoutMs"?: number }`. | -| `POST` | `/api/spawned/{name}/subscribe` | Subscribe an agent to channels. Body `{ "channels": ["..."] }`. | -| `POST` | `/api/spawned/{name}/unsubscribe` | Unsubscribe an agent from channels. Body `{ "channels": ["..."] }`. | -| `POST` | `/api/agents/by-name/{name}/interrupt` | Interrupt an agent (not yet implemented — returns 501). | - -#### `POST /api/spawn` - -Body (fields accept both camelCase and snake_case): - -```json -{ - "name": "Alice", - "cli": "claude", - "model": "sonnet", - "args": ["--no-color"], - "task": "Read the README and summarize it", - "channels": ["#general"], - "cwd": "/Users/me/project", - "team": "demo", - "shadowOf": null, - "shadowMode": null, - "continueFrom": null, - "idleThresholdSecs": 0, - "skipRelayPrompt": false, - "restartPolicy": null, - "agentToken": null -} -``` - -`name` and `cli` are required. Returns `{ "success": true, ... }` on -success or `{ "success": false, "error": "..." }` with a non-2xx -status on failure. - -### PTY interaction - -| Method | Path | Purpose | -| ------ | -------------------- | ----------------------------------------------------------------- | -| `POST` | `/api/input/{name}` | Send raw bytes to an agent's PTY stdin. Body `{ "data": "..." }`. | -| `POST` | `/api/resize/{name}` | Resize an agent's PTY. Body `{ "rows": , "cols": }`. | -| `POST` | `/api/send` | Inject a relay message into an agent. | - -#### `POST /api/input/{name}` - -This is the keystroke channel. The `data` string is written to the -target agent's stdin verbatim — escape characters are passed through. - -```bash -curl -X POST localhost:3888/api/input/Alice \ - -H "X-API-Key: $RELAY_BROKER_API_KEY" \ - -d '{"data":"hello\n"}' -``` - -Returns 404 if the agent isn't found. - -#### `POST /api/send` - -```json -{ - "to": "Alice", - "message": "Please review PR #837", - "from": "Bob", - "thread": "thr_abc", - "workspaceId": "ws_demo", - "mode": "wait" -} -``` - -The message text field accepts any of `message`, `text`, `body`, or -`content`. The `mode` field accepts `wait` (default — queue and -inject when the agent is idle) or `steer` (inject immediately, -even mid-response). Returns 504 on a 30s broker timeout; 404 if -the target agent isn't registered. - -### Event stream - -| Method | Path | Purpose | -| ------ | -------------------- | ---------------------------------------------------- | -| `GET` | `/ws` | WebSocket: subscribe to broker events. | -| `GET` | `/api/events/replay` | HTTP-based replay of events since a sequence number. | - -#### `GET /ws` - -Upgrade to WebSocket and you'll receive every broker event as a JSON -text frame. The broker pings every 30s; respond with pong to stay -connected. - -To resume after a disconnect without missing durable events, include -the last sequence number you saw: - -``` -ws://localhost:3888/ws?sinceSeq=12345 -``` - -If the requested sequence is older than the replay buffer's window, -the first frame you receive will be: - -```json -{ "kind": "replay_gap", "requestedSinceSeq": 12345, "oldestAvailable": 14000, "seq": 14999 } -``` - -> **Note:** Two event kinds are **ephemeral** and never stored in the -> replay buffer: `worker_stream` (PTY output chunks — high frequency) -> and `delivery_active` (in-flight delivery progress). If you -> disconnect, you cannot replay them. - -#### Event kinds emitted on `/ws` - -Durable (replayable via `?sinceSeq=...`): - -| `kind` | When it fires | -| -------------------------------------- | ------------------------------------------------------ | -| `agent_spawned` | An agent was successfully spawned. | -| `agent_exit` / `agent_exited` | Worker process exited. | -| `agent_idle` | Worker has been quiet past `idleThresholdSecs`. | -| `agent_restarting` / `agent_restarted` | Worker is being restarted under the restart policy. | -| `agent_released` | Worker was released by `/api/spawned/{name}` DELETE. | -| `agent_permanently_dead` | Worker exhausted restart attempts. | -| `worker_ready` | Worker finished startup and is ready for input. | -| `worker_error` | Worker emitted an error frame. | -| `relay_inbound` | Inbound relay message routed to an agent. | -| `delivery_ack` | Message delivery acknowledged. | -| `delivery_verified` | Echo verification confirmed the message was delivered. | -| `delivery_failed` | Message delivery failed. | -| `delivery_dropped` | Delivery was dropped (e.g. agent gone). | -| `delivery_retry` | Delivery is being retried. | - -Ephemeral (broadcast only, no replay): - -| `kind` | When it fires | -| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -| `worker_stream` | A chunk of stdout from a wrapped CLI. Payload contains `stream` (`"stdout"`) and `chunk` (the raw bytes — typically still ANSI-escaped). | -| `delivery_active` | High-frequency progress events for in-flight deliveries. | - -## Worked example: control a spawned agent end-to-end - -```bash -KEY="$RELAY_BROKER_API_KEY" - -# 1. Start the broker -agent-relay up --port 3888 & - -# 2. Spawn Alice running claude -curl -sX POST localhost:3888/api/spawn \ - -H "X-API-Key: $KEY" \ - -d '{"name":"Alice","cli":"claude"}' - -# 3. Stream her PTY output (filter for her worker_stream frames) -websocat ws://localhost:3888/ws \ - -H "X-API-Key: $KEY" \ - | jq -r 'select(.kind=="worker_stream" and .name=="Alice") | .chunk' - -# 4. Send a keystroke to Alice's CLI -curl -sX POST localhost:3888/api/input/Alice \ - -H "X-API-Key: $KEY" \ - -d '{"data":"hello\n"}' - -# 5. Inject a relay message -curl -sX POST localhost:3888/api/send \ - -H "X-API-Key: $KEY" \ - -d '{"to":"Alice","from":"Bob","message":"please review #837"}' - -# 6. Release her -curl -sX DELETE localhost:3888/api/spawned/Alice \ - -H "X-API-Key: $KEY" -``` - -## Error envelope - -Failed responses return a consistent envelope: - -```json -{ - "error": { - "code": "agent_not_found", - "message": "no agent named Alice", - "statusCode": 404 - } -} -``` - -Common codes: `agent_not_found` (404), `invalid_request` (400), -`unsupported_operation` (400), `unauthorized` (401), `request_failed` -(400), `internal_error` (500).