Skip to content

HTTP chat endpoint — POST /api/chat

Why

The runtime was Discord-in, Discord-out. POST /webhook/{source} exists but is GitHub-event specific: it formats an event into a prompt and dispatches it, it does not do request/reply.

That made Discord the only place a human could hold a conversation with an agent. Rig Cockpit (rig-cockpit-ios) had no way to reach one, and a Cockpit chat surface — open the app, pick a tenant agent, talk to it — is the goal this unblocks.

What

createDashboard(character, memory, { webhookHandler, chatHandler }) now accepts a chat handler alongside the existing webhook handler, and serves it at POST /api/chat.

The agent loop is untouched. The handler calls the same agent.process() the Discord path calls, with the same context shape, so providers, tools, MCP servers and memory all behave identically.

Request

{ "message": "how is the rig", "sessionId": "optional", "userName": "optional" }

Response

{ "reply": "...", "sessionId": "..." }

sessionId is passed straight through to agent.process()'s context — the same field stream-consumer.js already uses — so multi-turn continuity needs no new machinery. A session id is generated when the caller omits one.

Status codes

Code Meaning
200 reply produced
400 body is not JSON, or message is missing/empty/not a string
401 CHAT_API_TOKEN is set and X-Chat-Token does not match
413 body over 32 KB
429 a chat is already in flight
502 the provider failed
503 CHAT_API_TOKEN is unset — the route is disabled

Decisions

Fails closed. Every call drives an LLM, so the route is cost-incurring. It stays disabled and returns 503 until CHAT_API_TOKEN is set, mirroring rig-conductor's RunPodClient.IsConfigured, which 503s its pod-control routes until the operator provisions the secret. The alternative — ship open, harden later — leaves a window in which an unauthenticated caller can spend the agent's quota.

The token comparison is constant-time and tolerates a length mismatch rather than throwing, and an unset token is never satisfied by an unset header. That last one is the trap worth naming: a naive configured === presented is true when both are undefined, which would silently open the route on any pod without the secret.

One chat in flight. The claude-cli provider spawns a subprocess per turn, so unbounded parallel chats are a one-line way to exhaust the host. Additional callers get 429. The slot is released in a finally, because releasing only on success means the first provider error wedges the endpoint at 429 for the lifetime of the pod.

Provider errors are not echoed to the caller. They routinely carry env var names, credential paths and prompt fragments. The detail is logged; the response says only that the provider failed. This reply crosses a network boundary to a phone.

No new public surface. The dashboard service stays ClusterIP. Cockpit reaches this through rig-cockpit-worker, which is already the app's only edge.

Configuration

Variable Effect
CHAT_API_TOKEN Shared secret. Unset disables the route entirely.

Callers present it as X-Chat-Token.

Not in this change

  • Streaming. A turn is a single request/reply. Long turns will feel slow in the app before they feel wrong; streaming is the follow-up if that becomes the complaint.
  • Per-tenant authorization. One token, one agent, one pod. Tenant separation today is deployment-level: each tenant agent is its own release with its own token. That is sufficient while each pod serves exactly one character, and it is not sufficient once an agent holds credentials — see the note below.

Follow-up this exposes

In this runtime the persona and the tool grant live in the same file. character.json carries both the personality and the tools block that decides what the agent may reach, and buildTools() turns those endpoint declarations into callable tools with no policy layer in between.

That is fine for a read-only agent. It stops being fine when an agent holds credentials, because the file describing how the agent talks is also the file that authorises what it can touch. A separate policy document — one the agent cannot see or edit, where deny wins — is the next structural piece, and it does not exist yet.