HTTP channel
# HTTP channel
HTTP is the default channel for agent trees and the `bonnie serve` command. It accepts JSON and streams newline-delimited JSON (NDJSON), not server-sent events.
```sh
bonnie serve --addr 127.0.0.1:8080 --journal .bonnie \
--model anthropic/claude-sonnet-4-5
curl -s http://127.0.0.1:8080/bonnie/v1/health
# {"ok":true,"status":"ready"}
```
Configure the provider key before starting. Health is liveness only: it does not read the journal or check model access.
## Routes
All paths below start with `/bonnie/v1`.
| Method | Path | Effect |
| --- | --- | --- |
| GET | `/health` | Public liveness |
| GET | `/info` | `agent` (optional), `version`, and mounted `channels` |
| POST | `/runs` | New run, or message through an address |
| GET | `/addresses/{address}` | Look up a binding; unknown address is 404 |
| POST | `/addresses/{address}` | Ensure a binding and pending run, without a model turn |
| GET | `/runs/{id}` | Durable state and latest boundary result |
| GET | `/runs/{id}/snapshot` | Conversation, state, and cursor from one replay |
| POST | `/runs/{id}` | Message to an exact existing run |
| POST | `/runs/{id}/respond` | Answer a waiting run |
| POST | `/runs/{id}/cancel` | Durable cancellation request |
| POST | `/runs/{id}/reset` | Retire the run and free its HTTP addresses |
| POST | `/runs/{id}/clear` | Remove model conversation context, keep run and files |
| POST | `/runs/{id}/compact` | Summarize older model context |
| GET | `/runs/{id}/stream` | NDJSON events; optional `?cursor=N` |
ID-addressed message, read, and answer routes never create a run. Reserved internal runs are not addressable. Cancel on an unknown or idle run is a harmless `not_active` result. Reset, clear, and compact return 204 on success.
## Request fields
`POST /runs` accepts:
| JSON field | Type | Meaning |
| --- | --- | --- |
| `text` | string | User input |
| `address` | string, optional | HTTP-local conversation key; create on first use |
| `operation_id` | string, optional | Authenticated start idempotency key; cannot accompany `address` |
| `title` | string, optional | First-turn listing title |
| `kind` | string, optional | First-turn surface kind, such as `dm` or `thread` |
| `context` | array of strings, optional | Input for this turn only, separate from history |
| `turn_policy` | string, optional | `steer` (default) or `queue` |
| `auth` | principal object, optional | Self-asserted identity only when no authenticator is configured |
`POST /runs/{id}` accepts only `text`, `context`, `turn_policy`, and `auth` from this table. Neither request type has a file-upload field. JSON body decoding is limited to 1 MiB. The run API decoder is not a strict schema validator: do not use acceptance of an unknown field as proof that it has an effect.
`channel.Principal` has no JSON tags. Its Go JSON field names are `Authenticator`, `Kind`, `ID`, and `Attributes`. Do not confuse these with the snake_case fields on run requests. A body principal is not proof of identity.
```sh
curl -s http://127.0.0.1:8080/bonnie/v1/runs \
-H 'Content-Type: application/json' \
-d '{"address":"browser-42","text":"Summarize the deployment plan.","context":["Use staging only."],"turn_policy":"queue"}'
```
The normal response contains `run_id`, `state`, and optional `turn_id`, `cursor`, `response`, `suspend`, and `usage`. Usage contains `input_tokens` and `output_tokens` when available. Address ensure and lookup replies contain the run ID and optional cursor, not a model result.
States are `pending`, `running`, `waiting`, `completed`, `failed`, `cancelled`, and `retired`. A completed run can receive another turn. Retirement is permanent.
## Answer a waiting run
A `suspend` object has `kind`, `prompt`, and optional `options`, `tool_call_id`, and `turn_id`. Use `/respond`, not the message route, to answer it:
```sh
curl -s http://127.0.0.1:8080/bonnie/v1/runs/RUN_ID/respond \
-H 'Content-Type: application/json' \
-d '{"responses":[{"turn_id":"TURN_ID_FROM_SUSPEND","text":"eu-west-1"}]}'
```
Each response accepts `text`, optional `turn_id`, and optional `approved`. For an approval, send an explicit Boolean verdict:
```json
{"responses":[{"turn_id":"TURN_ID_FROM_SUSPEND","text":"Use staging only.","approved":true}]}
```
`approved: false` is rejection. An absent `approved` field is not rejection; it is a text-only answer. Include the current suspension's turn ID to protect against a delayed answer for an older turn.
## Cancel and reset
```sh
curl -s http://127.0.0.1:8080/bonnie/v1/runs/RUN_ID/cancel \
-H 'Content-Type: application/json' -d '{"turn_id":"OBSERVED_TURN_ID"}'
```
The optional `turn_id` protects a later turn from a stale command. A cancellation reply has `status` and optional `run_id` and `turn_id`. Status is `requested`, `not_active`, or `stale`. `requested` returns 202; no-op replies return 200. This confirms the command was saved, not that work stopped. Wait for `cancelled`. Cancellation can withdraw a parked turn and cannot undo external actions.
Reset accepts an optional `{"reason":"Start a separate task"}` body. It retires the exact run and frees bindings in the HTTP namespace only. Clear keeps working files and the journal; it is not secure deletion. Compact requires agent compaction support.
## Subscribe before the first turn
Live model deltas cannot be recovered from conversation history. Ensure an address before sending input:
```sh
curl -s -X POST http://127.0.0.1:8080/bonnie/v1/addresses/browser-42
# Copy run_id and cursor from this reply.
curl -sN 'http://127.0.0.1:8080/bonnie/v1/runs/RUN_ID/stream?cursor=CURSOR'
# In another terminal:
curl -s http://127.0.0.1:8080/bonnie/v1/runs/RUN_ID \
-H 'Content-Type: application/json' -d '{"text":"Review the plan."}'
```
Encode an address containing `/` as one URL path segment, for example `browser%2F42`. Do not add a channel prefix.
Streams use `application/x-ndjson` and `Cache-Control: no-store`. Events contain `run_id`, `seq`, `type`, `time`, and optional `turn_id`, `text`, `state`, or `data`. Runtime types include `run_turn`, `run_cancel_requested`, `run_state`, `run_suspend`, `run_resume`, and `run_response`. Other types come from Kit lifecycle events.
A cursor is the last journal position seen, not a unique counter for every live delta. Use a non-negative integer. Durable events can be replayed after a restart. Live-only reasoning and tool deltas cannot. Keep the stream open across waiting states; disable proxy buffering and set suitable timeouts.
For a conversation view, GET `/snapshot` first, then stream after its cursor. The snapshot adds `messages`, with complete Kit message content parts, to the run response. It excludes live reasoning and tool activity. Compaction does not remove older messages from display history; clear, branch selection, and torn-step repair do affect that view. Snapshot replies are `no-store`.
## Authentication and authorization
There is no default bearer-token environment variable or built-in token policy. Configure a verifier in Go:
```go
// Imports: net/http, os, github.com/mark3labs/bonnie,
// github.com/mark3labs/bonnie/channel, and
// bonniehttp "github.com/mark3labs/bonnie/channel/http".
bonnie.New(
bonnie.WithHTTPAuthenticator(func(r *http.Request) (*channel.Principal, error) {
token := os.Getenv("AGENT_HTTP_TOKEN") // Host-defined variable.
if token == "" || r.Header.Get("Authorization") != "Bearer "+token {
return nil, bonniehttp.ErrUnauthenticated
}
return &channel.Principal{
Authenticator: "host-token", Kind: "app", ID: "operations",
}, nil
}),
).Serve()
```
This is a simple single-caller example, not a multi-tenant access policy. Use TLS. For OIDC or mutual TLS, validate the assertion or certificate and return the identity it proves. Returning `ErrUnauthenticated` produces 401. Other verifier errors produce 500. A nil principal with nil error permits an unattributed request.
The verifier covers every HTTP channel route except health, including routes mounted on a host's own mux. With it configured, body `auth` is ignored, not merged. Without it, body identity is unverified. The API does not automatically restrict exact run IDs to their original caller. Add access controls around routes and host tools before a multi-user deployment.
Low-level servers can build `bonniehttp.New(runner, bonniehttp.WithAuthenticator(fn))` and mount `ch.Handler()`. `HandlerWithOutbound` also supplies a registry for hand-offs. Custom channels cannot mount routes under the reserved `/bonnie/` namespace.
## Idempotent starts
`operation_id` requires a non-nil verified principal from the channel authenticator. A reverse proxy alone does not make the field valid unless the channel verifier returns a proven principal.
```sh
curl -s http://127.0.0.1:8080/bonnie/v1/runs \
-H "Authorization: Bearer $AGENT_HTTP_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"operation_id":"deploy-request-42","text":"Prepare a staging deployment."}'
```
The same key under the same `Authenticator`, `Kind`, and `ID` returns saved state without another turn. Changed text on a retry does not change the admitted operation. This also applies after failure, cancellation, retirement, or a restart. Use a new key for new work. A crash before execution can leave `pending`; recovery is explicit through the run API. This is not distributed exactly-once execution: keep one live owner for the journal's runs.
## Errors
Non-2xx channel replies use `{"error":"human-readable text","code":"stable_code"}`. Branch on `code`, not the text.
| Status | Representative codes |
| --- | --- |
| 400 | `bad_request`, `invalid_run_id`, `unknown_turn_policy` |
| 401 | `unauthenticated` |
| 404 | `run_not_found` |
| 405 | `method_not_allowed` |
| 409 | `run_waiting`, `run_not_waiting`, `run_active`, `run_not_active`, `run_retired`, `run_owned_elsewhere`, `conversation_corrupt` |
| 413 | `too_large` |
| 499 | `client_closed` |
| 501 | `files_unsupported`, `compaction_unsupported` |
| 500 | `internal` |
Unexpected internal details go to server logs, not the response. A transport timeout does not prove an external action did not occur.
## Schedule routes
When schedules are configured, GET `/bonnie/v1/schedules` lists them and GET `/bonnie/v1/schedules/{name}` returns definition and history. POST `/{name}/trigger` is mounted only with `WithScheduleTriggerAuthorizer`. This callback authorizes a trigger separately from the normal HTTP authenticator; configure both as needed.
Manual triggers may send `{}`. External triggers require a stable `id`, RFC3339 `scheduled_at`, and `kind: "external"`. Retry with the same ID and scheduled time. Unlike run request decoding, trigger decoding rejects unknown fields and extra JSON values. See [scheduled channel work](/channels/overview#scheduled-channel-work) for delivery limits.
## Source and tests
[HTTP godoc](https://pkg.go.dev/github.com/mark3labs/bonnie/channel/http). Verified against `channel/http/http.go`, `snapshot.go`, `runtime/events.go`, `runtime/suspend.go`, and `runtime/cancellation.go`. Tests include `auth_test.go`, `ensure_test.go`, `snapshot_test.go`, `operation_test.go`, `cancel_test.go`, and `limits_test.go` under `channel/http`.