---
title: HTTP control plane
summary: How to think about Holon's headless integration surface.
order: 20
---
# HTTP control plane
Holon is designed to be headless. HTTP and event-driven integration surfaces
should preserve the same runtime concepts as the CLI: origin, trust, priority,
work items, tasks, queues, wakeups, and user-facing delivery.
## Authentication
When a control token is configured (e.g. `--token`, `--token-file`, or the
`control_token` config key), the HTTP server operates in **bearer mode**.
All `/control/*` routes require an `Authorization: Bearer <token>` header,
and read-only routes (agent state, events, tasks) require it for remote
access as well. Without a control token, the server runs in **local mode**
and trusts the local process boundary.
```
## Endpoint reference
### Discovery
**`GET /`** — Root
Returns the default agent ID.
```json
{ "ok": true, "default_agent": "main" }
```
**`GET /handshake`** — Protocol handshake
Returns auth mode, capabilities, and runtime info.
```json
{
"ok": true,
"protocol": { "name": "holon-control", "version": 1 },
"auth": { "mode": "bearer", "required": true },
"capabilities": ["agents.list", "agents.state", "agents.events", "agents.control", "tui.remote"],
"runtime": {
"default_agent": "main",
"workspace_dir": "/path/to/workspace",
"home_dir": "/path/to/holon/home",
"listen": "127.0.0.1:9101",
"advertise_url": null
}
}
```
**`GET /models`** — Available models
Returns model catalog and runtime availability.
```json
{
"available_models": [
{ "id": "claude-sonnet-4-20250514", "display_name": "Claude Sonnet 4", … }
],
"model_availability": { "claude-sonnet-4-20250514": true, … }
}
```
### Agents
**`GET /agents/list`** — List agent entries
Returns lightweight public agent entries for selection and navigation without
loading full per-agent runtime summaries.
**`GET /agents/:id/status`** — Single agent status
Returns the same `AgentSummary` shape for the named agent.
**`GET /agents/:id/state`** — Full agent state snapshot
Returns a combined state page: agent summary, session info (current run, pending
count), active tasks, recent timers, work items, waiting intents, external
triggers, operator notifications, execution snapshot, and workspace occupancy.
**`GET /agents/:id/briefs`** — Recent briefs
Returns recent briefs (acknowledgements and results) for the agent.
**`GET /agents/:id/tasks`** — Active tasks
Returns active and recent tasks with status, kind, and timing metadata.
**`GET /agents/:id/timers`** — Recent timers
Returns recent timer records.
**`GET /agents/:id/events`** — Event log
Returns recent runtime events (turn entries, system events). Query parameters:
| `before_seq` | Return events with durable `event_seq` lower than this value |
| `after_seq` | Return events with durable `event_seq` higher than this value |
| `limit` | Max events to return (default 128) |
| `order` | `asc` or `desc` (default) |
| `projection` | `local_debug` (control token required) or `operator` (default) |
**`GET /agents/:id/events/stream`** — Server-sent events
SSE stream of agent events. Supports `after_seq`, `limit`, and `projection`
query params. The SSE `id` field is the per-agent durable `event_seq`, and the
SSE `event` field is set to the raw audit event kind (e.g. `turn_entry`,
`wake_requested`, `task_create_requested`), not a limited set of names.
**`GET /agents/:id/transcript`** — Turn transcript
Returns the current turn transcript entries.
**`GET /agents/:id/worktree-summary`** — Worktree summary
Returns managed worktree entries for the agent's workspace.
### Enqueue (public ingress)
**`POST /agents/:id/enqueue`** — Enqueue a message
Accepts external callers on the public HTTP surface. When the server is in
**bearer mode**, this route calls `authorize_remote_access` and requires the
control token just like read-only routes. In **local mode** no auth header is
needed.
The runtime classifies origin, trust, and priority; public callers may not
override trust or use `interject` priority.
Request shape:
```json
{
"kind": "channel_event | webhook_event",
"json": { "structured": "body" },
"body": { "type": "text", "text": "…" },
"origin": {
"kind": "channel",
"channel_id": "slack-general",
"sender_id": "U123"
},
"metadata": {},
"correlation_id": "optional-correlation",
"causation_id": "optional-causation"
}
```
Response:
```json
{ "ok": true, "agent_id": "main", "message_id": "msg-abc123" }
```
**`POST /enqueue`** (no agent in path) — Enqueue to default agent.
### Control plane (authenticated)
All `/control/*` routes require a control token when the server is in bearer
mode.
**`POST /control/agents/:id/prompt`** — Send an operator prompt
Sends a prompt that enters the agent queue as an operator message with
`trusted_operator` classification.
```json
{ "text": "What is the current status?" }
```
**`POST /control/agents/:id/wake`** — Explicit wake
Wakes a sleeping agent with a control-plane wake hint.
```json
{ "reason": "manual-wake", "source": "operator" }
```
Response:
```json
{ "ok": true, "agent_id": "main", "disposition": "woken" }
```
**`POST /control/agents/:id/control`** — Control action
Sends a control action. Request body:
```json
{ "action": "stop", "trust": "trusted_operator" }
```
**`POST /control/agents/:id/current-run/abort`** — Abort current run
Aborts the current agent run loop. New callers should use
`mode: "stop_after_abort"`. The legacy `pause_after_abort` value is accepted as
a compatibility alias and is treated as `stop_after_abort`.
```json
{ "mode": "stop_after_abort" }
```
**`POST /control/agents/:id/create`** — Create agent
Creates a new agent managed by the host. The agent id comes from the URL path.
```json
{ "template": null, "trust": "trusted_operator" }
```
**`POST /control/agents/:id/tasks`** — Create command task
Starts a background command task for the agent.
```json
{
"summary": "Build project",
"cmd": "cargo build",
"workdir": null,
"shell": null,
"login": false
}
```
**`POST /control/agents/:id/work-items`** — Create work item
Creates a durable work item for the agent. Only `objective` is accepted.
```json
{
"objective": "Fix the build",
"trust": "trusted_operator"
}
```
**`POST /control/agents/:id/timers`** — Create timer
Creates a timer that will deliver a `TimerTick` to the agent.
```json
{
"duration_ms": 60000,
"interval_ms": null,
"summary": "reminder",
"trust": "trusted_operator"
}
```
**`POST /control/agents/:id/debug-prompt`** — Debug prompt
Sends a debug-mode prompt (runtime-internal classification). Request body:
```json
{ "text": "debug instruction", "trust": "trusted_operator" }
```
**`POST /control/agents/:id/operator-bindings`** — Create operator transport binding
Sets up a callback URL or transport binding for operator notifications.
Request body:
```json
{
"binding_id": "my-binding",
"transport": "http-callback",
"operator_actor_id": "operator-1",
"default_route_id": "default",
"delivery_callback_url": "https://example.com/callback",
"delivery_auth": { "type": "bearer", "token": "secret" },
"capabilities": { "send_prompt": true },
"provider": "anthropic",
"provider_identity_ref": "user-123",
"metadata": {}
}
```
**`POST /control/agents/:id/operator-ingress`** — Operator ingress
Direct ingress path for operator-origin messages through the control plane.
Request body:
```json
{
"text": "operator message",
"actor_id": "operator-1",
"binding_id": "my-binding",
"reply_route_id": "route-1",
"provider": "anthropic",
"correlation_id": "corr-123"
}
```
**`POST /control/agents/:id/workspace/attach`** — Attach workspace
```json
{ "path": "/path/to/workspace" }
```
**`POST /control/agents/:id/workspace/exit`** — Exit current workspace
Returns to the agent home workspace. Accepts an optional body:
```json
{ "trust": "trusted_operator" }
```
**`POST /control/agents/:id/workspace/detach`** — Detach workspace
Removes a workspace registration without switching. Request body:
```json
{ "workspace_id": "ws-abc123", "trust": "trusted_operator" }
```
**`POST /control/agents/:id/model`** — Set agent model
```json
{ "model": "claude-sonnet-4-20250514" }
```
**`POST /control/agents/:id/model/clear`** — Clear model override
Reverts to the default model. Accepts an optional body:
```json
{ "trust": "trusted_operator" }
```
### Runtime management
**`GET /control/runtime/status`** — Runtime status
Returns daemon and runtime health info including configured models, control
token status, and activity markers.
**`POST /control/runtime/shutdown`** — Graceful shutdown
Shuts down the runtime and daemon gracefully.
### Webhooks & callbacks
**`POST /webhooks/generic/:agent_id`** — Generic webhook
Accepts arbitrary JSON payloads and enqueues them as `WebhookEvent` messages
to the named agent. Useful for GitHub webhooks, CI notifications, and external
service integrations.
**`POST /callbacks/enqueue/:callback_token`** — Callback enqueue
Receives enqueue callbacks from registered callback URLs. Body limit: 256 KB.
**`POST /callbacks/wake/:callback_token`** — Callback wake
Receives wake callbacks from registered callback URLs.
## Message shapes
### MessageKind
Valid enqueue kinds for external callers: `channel_event`, `webhook_event`.
The policy only allows `operator_prompt` with an operator origin; public
enqueue rejects it. Runtime-owned kinds (`system_tick`, `task_result`,
`task_status`, `control`, `internal_followup`) are rejected from
external enqueue.
### Priority
| `interject` | Preempts normal queue; control-plane only |
| `next` | After current turn, before queued |
| `normal` | Standard queue position |
| `background` | Low urgency, processed when idle |
### TrustLevel
| `trusted_operator` | `operator` | Direct operator action |
| `trusted_system` | `system`, `task`, `timer` | Runtime-internal action |
| `trusted_integration` | `webhook`, `callback` | Known integration with explicit trust |
| `untrusted_external` | `channel` | Public channel / unauthenticated caller |
### MessageOrigin
| `operator` | `actor_id` (optional) |
| `channel` | `channel_id`, `sender_id` (optional) |
| `webhook` | `source`, `event_type` (optional) |
| `callback` | `descriptor_id`, `source` (optional) |
| `timer` | `timer_id` |
| `system` | `subsystem` |
| `task` | `task_id` |
### MessageBody
| `text` | `text: string` |
| `json` | `value: object` |
| `brief` | `title`, `text`, `attachments` |
## Design goals
- Keep transport details outside the core runtime model.
- Preserve provenance for inbound messages and external events.
- Return structured lifecycle state rather than only streaming text.
- Make wake, sleep, enqueue, and task supervision visible to integrations.
- Keep user-facing output separate from internal traces.
## Integration posture
Treat the HTTP surface as a control plane for runtime state, not a chat-only
endpoint. A good integration should be able to ask:
- What work is active?
- Which tasks are running or waiting?
- What event woke the agent?
- Which output is safe to show to a user?
- Which evidence is internal runtime detail?
### Routes not yet documented
The following routes exist in `src/http.rs` but are not yet fully documented
on this reference page:
- `GET /agents/:id/skills`
- `POST /control/agents/:id/skills/install`
- `POST /control/agents/:id/skills/uninstall`
- Default-agent aliases: `/status`, `/briefs`, `/state`, `/transcript`,
`/worktree-summary`
These will be added as the surface stabilizes.
## Common curl examples
```bash
# Check server health
curl http://127.0.0.1:9101/handshake
# List agents
curl http://127.0.0.1:9101/agents/list
# Get agent state
curl http://127.0.0.1:9101/agents/main/state
# Send a prompt (control token required)
curl -X POST http://127.0.0.1:9101/control/agents/main/prompt \
-H "Authorization: Bearer $HOLON_CONTROL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "Run cargo check"}'
# Enqueue via webhook (public)
curl -X POST http://127.0.0.1:9101/webhooks/generic/main \
-H "Content-Type: application/json" \
-d '{"event": "ci-complete", "status": "success"}'
# Stream agent events
curl -N http://127.0.0.1:9101/agents/main/events/stream
```