> ## Documentation Index
> Fetch the complete documentation index at: https://internal.september.wtf/llms.txt
> Use this file to discover all available pages before exploring further.

# Engine contract

> What the orchestrator promises engines and what engines promise back.

bap-engine and the Engine talk over plain HTTP. The contract between
them is small and stable: the orchestrator launches engine containers
with a known set of environment variables and mounts; the engine
exposes a known set of HTTP endpoints. This page is the contract.

It's stable across patch releases of either side. Major version bumps
on the engine require coordinated bumps on the orchestrator's
`ORCH_ENGINE_IMAGE`.

## What the orchestrator gives an engine

When the orchestrator launches an engine container, it provides:

### Environment variables (always)

| Variable          | Source                      | Purpose                                                                                     |
| ----------------- | --------------------------- | ------------------------------------------------------------------------------------------- |
| `ENGINE_KEY_HASH` | Generated by orchestrator   | SHA-256 of the engine's API key. The engine validates incoming `X-Engine-Key` against this. |
| `SQLITE_DB_PATH`  | Always `/data/brain.sqlite` | Where to write the per-user brain.                                                          |
| `CATALOG_DIR`     | Always `/catalog`           | Where to read agents, skills, tools, soul template.                                         |

### Environment variables (passed through)

The orchestrator forwards any variable named in
`ORCH_ENGINE_ENV_PASSTHROUGH`. The standard set:

* `LLM_API_KEY`
* `OPENAI_API_KEY`
* `ANTHROPIC_API_KEY`
* `GEMINI_API_KEY`

The engine reads these directly from its own environment via
`src/config.py`. The orchestrator doesn't transform them.

### Mounts

| Mount                                              | Mode | Purpose                                                           |
| -------------------------------------------------- | ---- | ----------------------------------------------------------------- |
| `engine-data-{engine_id}` (named volume) → `/data` | `rw` | Per-user brain SQLite + any other persistent files.               |
| `<host catalog dir>` → `/catalog`                  | `ro` | Shared platform catalog (agents, skills, tools, core\_soul.json). |

### Network

* The container joins the Docker network specified in
  `ORCH_ENGINE_NETWORK` (default `engine_net`).
* The engine's port (typically `8000` inside the container) is mapped
  to a host port in `[ORCH_PORT_MIN, ORCH_PORT_MAX]`, bound to
  `127.0.0.1`.

### Lifecycle expectations

* The container is created with `restart: unless-stopped`, so a crash
  inside the container restarts it. The orchestrator's auto-restart is
  for the case where the container exits and stays exited, or fails
  health checks despite running.
* The orchestrator can stop, start, and destroy at any time. Engines
  must support clean shutdown via SIGTERM.

## What the engine gives back

The engine must implement these endpoints and behaviors. Stability
guarantees are documented per endpoint.

### `GET /health`

**Auth:** none.

**Stability:** strict — schema and semantics never change.

**Behavior:** returns `{"status": "ok", "uptime_seconds": float,
"subsystems": {...}}` when the engine is fully ready to serve
`/execute`. Returns non-200 or `status != "ok"` when not.

The orchestrator polls this during provisioning to confirm boot
completion (up to `ORCH_BOOT_TIMEOUT_S`, default 60s) and during the
background health loop.

### `POST /execute`

**Auth:** `X-Engine-Key` header.

**Stability:** breaking changes to the request body or the SSE event
shapes require a major version bump. New optional request fields and
new event types are minor additions.

**Body:**

```json theme={null}
{
  "message": "string",
  "task_id": "string",
  "max_turns": 50,
  "super_thinking": false,
  "media": [],
  "direct": false,
  "edit_message_id": null,
  "retry_message_id": null
}
```

**Response:** `text/event-stream`. See
[Streaming events](../platform/api-reference/streaming-events).

### `POST /rpc`

**Auth:** `X-Engine-Key` header.

**Stability:** the dispatcher shape is stable; individual operations
are versioned per `op` name.

**Body:**

```json theme={null}
{
  "subsystem": "control" | "telemetry",
  "op": "string",
  ...op-specific params
}
```

The orchestrator uses this for control operations:

* `set_config` — atomic swap of the gateway config.
* `get_config` — read the active config.
* `reload` — re-read `/config/gateway.yaml` from disk.
* `quota_state` — current quota usage.
* `cache_state` — prompt-cache efficiency.

These are how the orchestrator (or your product) reads runtime state
from the engine without restarting it.

## Versioning

Both sides follow semver:

| Engine version                    | Orchestrator behavior                                                |
| --------------------------------- | -------------------------------------------------------------------- |
| `MAJOR` bump (e.g. 2.x → 3.x)     | Update `ORCH_ENGINE_IMAGE` + run regression evals before rolling.    |
| `MINOR` bump (e.g. 2.3 → 2.4)     | Compatible. New optional features available; orchestrator unchanged. |
| `PATCH` bump (e.g. 2.3.0 → 2.3.1) | Compatible. Bug fixes only.                                          |

The orchestrator pins to a specific engine version via
`ORCH_ENGINE_IMAGE`. Don't run `latest` — version drift is the most
common source of subtle bugs.

## Auth flow

```mermaid theme={null}
sequenceDiagram
    participant Product
    participant Orch as bap-engine
    participant Engine

    Product->>Orch: POST /engines/{user_id}/admit (X-Platform-Key)
    Orch->>Orch: hash key, look up product
    Orch->>Orch: check policy (quota, rate-limit)
    alt no engine yet, auto_provision=true
        Orch->>Orch: generate ENGINE_KEY (sk-sept-...)
        Orch->>Orch: hash + Fernet-encrypt
        Orch->>Engine: launch container with ENGINE_KEY_HASH
        Orch->>Engine: poll GET /health
        Engine-->>Orch: 200 ok
    end
    Orch->>Product: {admitted: true, engine: {url, api_key, ...}}
    Product->>Engine: POST /execute (X-Engine-Key, the api_key)
    Engine->>Engine: hash X-Engine-Key, compare to ENGINE_KEY_HASH
    Engine-->>Product: SSE stream
```

The orchestrator generates the engine API key, sends only the hash to
the engine, and returns the plaintext to the product. The engine
never sees the raw orchestrator credentials; the product never sees
the engine's hash.

## What the contract does **not** cover

* The engine's *internal* APIs — agent loop, memory, sandbox. Those
  evolve without coordination with the orchestrator.
* Per-product config beyond what's in `gateway.yaml`. If you need
  per-product behavior in the engine, do it in the product layer
  (BAP) or via a richer config file.
* Streaming HTTP/2. The contract is HTTP/1.1 + chunked encoding for
  SSE.

## See also

* [Architecture](./architecture) — how the orchestrator is built.
* [Lifecycle](./lifecycle) — the engine state machine.
* [Auth](./api-reference/auth) — the orchestrator's own auth.
* [Engine API reference](../platform/api-reference/overview) — the
  engine's own surface.
