Skip to content

HTTP Trigger Server

chaos serve turns Chaos into a webhook target.

chaos serve --bearer-token <TOKEN> [OPTIONS]

One request → one process → one response. No sessions, no streaming.

A bearer token is required - set it via --bearer-token or the CHAOS_BEARER_TOKEN env var. An empty token is rejected at startup.

Flag Default Notes
--bind 127.0.0.1 IPv4 or IPv6 (::1, 0.0.0.0)
--port 4000
--bearer-token env CHAOS_BEARER_TOKEN Required. Empty = rejected at startup.
--timeout 600 Wall-clock seconds per trigger (covers start + execution).
--max-concurrent 4 Semaphore-based. Excess requests get 429 immediately.
--body-limit 1048576 Bytes. Pre-checked via Content-Length, enforced post-read.
-m, --model config default Server-wide. Per-request override is rejected.
--sandbox config default Sandbox policy for spawned commands.
--skip-git-repo-check false
--ephemeral false No session persistence.
-C, --cd cwd Working directory for triggered processes.

Root-level flags (--provider, -c key=value, etc.) work as with other subcommands.

No auth. Returns 200 after startup validation completes.

{"status": "ok", "version": "47.0.0"}
Authorization: Bearer <token>
Content-Type: application/json

Request:

{
"request": "Review the latest PR and post feedback",
"caller_session_id": "optional",
"conversation_id": "optional",
"requested_by": "[email protected]",
"metadata": {}
}
  • request - required, non-empty. Alias: prompt.
  • caller_session_id - correlation field, echoed back. Alias: session_id.
  • conversation_id - auto-generated UUID if omitted. Always returned.
  • requested_by - recorded in tracing spans.
  • metadata - opaque JSON, recorded in spans.
  • model - rejected with 400 if present (the model is server-wide).

Response (200):

{
"status": "ok",
"caller_session_id": "...",
"conversation_id": "...",
"process_id": "uuid",
"result": "Agent output text",
"usage": {
"total_token_usage": { "input_tokens": 1200, "cached_input_tokens": 300, "output_tokens": 450, "reasoning_output_tokens": 0, "total_tokens": 1650 },
"last_token_usage": { "input_tokens": 1200, "cached_input_tokens": 300, "output_tokens": 450, "reasoning_output_tokens": 0, "total_tokens": 1650 },
"model_context_window": 200000
}
}

usage is null when the provider doesn’t report token counts.

All errors are JSON. caller_session_id and conversation_id are included when available; process_id when a process was started.

Status Condition
400 Bad JSON, empty request, unsupported model field, wrong Content-Type
401 Missing/wrong bearer token. Includes WWW-Authenticate: Bearer.
405 Wrong method on known route. Includes Allow header.
413 Body exceeds --body-limit
429 Concurrency limit hit
500 Process error (agent failure, runtime crash). Internal details are logged, not returned.
504 Timeout exceeded. Process is cleaned up.

Runs in-process via ProcessTable::start_process - same runtime path as chaos exec, no subprocess.

POST /api/trigger
→ auth → content-type → body limit → deserialize → validate
→ acquire semaphore permit
→ timeout_at(deadline) { start process → submit prompt → drain events }
→ cleanup process (bounded 30s grace, always removes from ProcessTable)
→ respond

The process lifecycle is split: runner::start creates the process handle, runner::execute submits and drains events, runner::cleanup shuts down and removes from the table. The API layer owns the handle across all three phases, so timeout cancellation always cleans up.

Headless mode (ApprovalPolicy::Headless) auto-approves tool use. Interactive events (approval requests, elicitations) are logged as warnings and skipped.

Expected behind a reverse proxy (nginx, Caddy, k8s ingress). No TLS, no rate limiting beyond the semaphore.

  • CHAOS_BEARER_TOKEN - bearer token environment variable
  • ~/.chaos/log/ - runtime logs for diagnosing server failures