# Deployment

Operations guide for running the PRIOR engine in production. PRIOR ships as a **single immutable
container** with a **native Rust serving path**: no Python, no Node, and no external calls at
inference time. Default port **`8089`**.

> This supersedes any older "Python gateway on port 8000" material. Production is the native engine.

---

## 1. The container model

The image bakes everything needed to enforce policy *except* the generation weights (which are large
and shared, so you mount them):

| Baked into the image | Supplied at run time |
|---|---|
| The `prior` Rust binary + patched llama libraries | The **generation GGUF** (mounted read-only) |
| The MiniLM encoder GGUF (the gate's embedder) | A **`license.bin`** node-locked to your host |
| The default policy packs (`harm_veto`, `codeops_minilm`, …) | Env configuration (below) |
| The model steering registry (`model_params.json`) | A **writable state volume** at `/app/state` |

Writable state (the license identity record, the SQLite governance DB, and live re-calibration
output) lives under **`/app/state`**, the only directory the non-root `prior` user owns. **Mount a
volume there.** It is not optional on a node-locked tier: the identity record is what binds this
license to this node, and a container that starts from an empty state volume derives a fresh node
identity, which seat enforcement reads as a *second* node. The activation is then denied and the
engine degrades to unsteered passthrough. You also lose your admin keys and audit log.

### Minimal run (docker compose)

```bash
MODELS_DIR=/srv/models MODEL=Llama-3.2-3B-Instruct-Q4_K_M.gguf \
  docker compose up -d
```

### Minimal run (docker run)

```bash
docker run -d --name prior \
  -p 127.0.0.1:8089:8089 \
  -e PRIOR_NATIVE_MODEL=/models/Llama-3.2-3B-Instruct-Q4_K_M.gguf \
  -v /srv/models:/models:ro \
  -v prior-state:/app/state \
  ghcr.io/eagle-logic/prior:slim-cpu
```

### GPU

Use the CUDA image and offload layers to the GPU:

```bash
docker run -d --name prior --gpus all \
  -p 127.0.0.1:8089:8089 \
  -e PRIOR_NATIVE_MODEL=/models/Llama-3.2-3B-Instruct-Q4_K_M.gguf \
  -e PRIOR_NATIVE_NGL=99 \
  -v /srv/models:/models:ro -v prior-state:/app/state \
  ghcr.io/eagle-logic/prior:slim
```

The reference 7–8B model at 4-bit needs ~6 GB VRAM; run **one model per GPU**.

---

## 2. Network exposure & auth

**The published port is loopback-only by default.** `docker-compose.yml` publishes to
`127.0.0.1:8089`, which is reachable for local testing but not on the network. This is intentional:
the `/v1` endpoint is **unauthenticated by default**.

To expose the engine on a LAN or the internet, do **both**:

1. Publish on a routable interface: `PRIOR_PUBLISH_ADDR=0.0.0.0`.
2. Require a key on `/v1`: `PRIOR_REQUIRE_AUTH=1` (then create keys via `/admin/keys`).

Put TLS termination (a reverse proxy / load balancer) in front for anything internet-facing.

---

## 3. Licensing (node-locked)

The engine verifies an **ED25519-signed** license bound to the host's hardware fingerprint before
enabling steering. Eagle Logic's public key is **baked into the published image**, so it verifies
**locally** and **never calls home in the inference path**, and you do **not** supply a key. (Connected
tiers make a periodic background license check-in; the air-gapped tier makes none and runs fully
offline (see [Licensing](licensing.md)).) You only mount your `license.bin`:

```bash
docker run -d --name prior --gpus all \
  -p 127.0.0.1:8089:8089 \
  -e PRIOR_NATIVE_MODEL=/models/model.gguf \
  -v /srv/models:/models:ro \
  -v /path/to/license.bin:/app/license.bin:ro \
  -v prior-state:/app/state \
  ghcr.io/eagle-logic/prior:slim
```

- **Without a valid license**, PRIOR degrades to plain inference with **no gate actuation**
  (`x-prior-steering: disabled`) rather than failing to boot.
- **There is no bypass.** A license verifies or it does not load, local development included.
- A license binds customer id, hardware id, tier, expiry, and capabilities. Expired, tampered, or
  wrong-host licenses are rejected. Full details in **[Licensing](licensing.md)**.

---

## 4. Environment variable reference

Everything is optional except where noted. Defaults shown are the container defaults.

### Server & network

| Variable | Default | Purpose |
|---|---|---|
| `PRIOR_PORT` | `8089` | HTTP port. |
| `PRIOR_BIND_HOST` | **`127.0.0.1`** | Interface the server binds *inside* the container. The default is the container's own loopback, which no `-p` mapping can reach — **set `0.0.0.0` under `docker run`** or the engine starts fine and refuses every connection. Compose sets it for you. |
| `PRIOR_PUBLISH_ADDR` | `127.0.0.1` | Host interface the port is published on (compose). Set `0.0.0.0` to expose. |
| `PRIOR_ALLOWED_ORIGINS` | `*` | CORS allowed origins (comma-separated). Restrict in production. |

### Model & engine

| Variable | Default | Purpose |
|---|---|---|
| `PRIOR_POLICY` | `on` | Master policy switch. `on` = the shipped detect+steer policy (harmful requests and destructive ops refused); `off` = byte-for-byte passthrough. Detection and steering always move together. |
| `PRIOR_NATIVE_MODEL` | (none) | Path to the generation GGUF (**required**). |
| `PRIOR_NATIVE_ENCODER` | baked MiniLM | Path to the gate's encoder GGUF. |
| `PRIOR_NATIVE_ARBITERS` | `harm_veto`, `codeops_minilm`, `injection.byteppm` | Comma-separated arbiter pack paths to load at boot. The image sets all three; override to load your own. |
| `PRIOR_NATIVE_NGL` | `0` | GPU layers to offload for generation (`99` = all; `0` = CPU). |
| `PRIOR_NATIVE_ENCODER_NGL` | `0` | GPU layers for the encoder. |
| `PRIOR_MAX_CONTEXT` | `8192` | Max context window. |
| `PRIOR_MODEL_REGISTRY` | `/app/state/model_params.json` | Per-model steering registry (golden layer + alpha). |
| `PRIOR_SPAN_SCORING` | `1` | Span-score padding defense on/off. |
| `PRIOR_SPAN_K` | `12` | Span-scoring neighbor count. |
| `PRIOR_GRAD_MAX_ANCHOR` | `256` | Max anchor length for the steering gradient. |

### Auth & license

| Variable | Default | Purpose |
|---|---|---|
| `PRIOR_REQUIRE_AUTH` | `0` | Require a Bearer key on `/v1` (live-toggleable via `/admin/runtime`). |
| `PRIOR_API_KEY` | (none) | Seed an Admin key at startup (env-only, not persisted). |
| `PRIOR_LICENSE_PUBLIC_KEY` | **removed** | No longer read. The verification key is compiled into the engine; it was an env var until it became clear a published image could be pointed at any key. Setting it has no effect. |
| `PRIOR_LICENSE_FILE` | auto-search | Path to `license.bin`. |
| `PRIOR_LICENSE_REFRESH_URL` | (none) | Optional connected-tier license auto-refresh endpoint. |
| `PRIOR_LICENSE_REFRESH_TICK_SECS` | `3600` | Refresh poll interval. |
| `PRIOR_LICENSE_REFRESH_THRESHOLD_DAYS` | `7` | Days before expiry to trigger a refresh. |

### Storage & state

| Variable | Default | Purpose |
|---|---|---|
| `PRIOR_STATE_DIR` | `/app/state` | Writable state directory (owned by the non-root user). |
| `PRIOR_DB_PATH` | `/app/state/ssa_oracle.db` | Governance SQLite DB (keys, audit). `:memory:` for ephemeral. |

### Observability & safety

| Variable | Default | Purpose |
|---|---|---|
| `PRIOR_LOG_LEVEL` / `RUST_LOG` | `info` | Log verbosity. |
| `PRIOR_JSON_LOGS` | (none) | Emit structured JSON logs (set to enable). |
| `PRIOR_YELLOW_MODE` | `inline` | YELLOW confirm delivery: `inline` (append to content) or `header` only. |
| `PRIOR_SAFETY_AUDIT_RETAIN` | `1000` | Rows retained in the durable safety audit. |
| `PRIOR_DIAG_TOKEN` | (none) | Optional token for privileged diagnostics. |

### Policy ingest (extractor)

Only needed to use `POST /admin/packs/ingest`. Unconfigured ⇒ a clear `400`, never a silent dial.

| Variable | Purpose |
|---|---|
| `PRIOR_EXTRACTOR_URL` | Extraction LLM endpoint (llama.cpp `/completion` or OpenAI-compatible). |
| `PRIOR_EXTRACTOR_ENDPOINT` / `_CHAT` | API path / chat-style toggle. |
| `PRIOR_EXTRACTOR_TEMPERATURE` / `_MAX_TOKENS` / `_TIMEOUT` | Sampling + limits. |

---

## 5. Health, readiness & Kubernetes

| Probe | Endpoint | Behavior |
|---|---|---|
| **Liveness** | `GET /health` | Answers as soon as the HTTP server is up (the model loads lazily). |
| **Readiness** | `GET /health` | Use the same endpoint; back it with a `startupProbe` budget generous enough for a cold model load/download. |

```yaml
livenessProbe:
  httpGet: { path: /health, port: 8089 }
  periodSeconds: 10
startupProbe:
  httpGet: { path: /health, port: 8089 }
  failureThreshold: 120     # tolerate a cold-node model load
  periodSeconds: 5
```

Because the engine is stateless per request, scale horizontally by running more replicas behind a
load balancer; each replica needs its own node-locked license (or a cluster license; see
[Licensing](licensing.md)).

---

## 6. Observability

- **Metrics.** `GET /metrics` is Prometheus exposition: zone distribution (GREEN/YELLOW/RED), refusal
  counts, and latency / token / injection histograms. Scrape it directly.
- **Structured logs.** Set `PRIOR_JSON_LOGS` for one JSON object per line on stdout, ready for
  Datadog / Splunk / ELK. Routing decisions and steering-apply events are structured fields.
- **Safety audit.** `GET /admin/audit/safety` is the durable, queryable trail of every gate decision
  (prompt **hashes** only, never text or completions). Pair `x-prior-decision-id` from a response
  with the matching audit row for end-to-end traceability.

---

## 7. Upgrades

The container is the atomic unit. To upgrade, pull the new image and replace the container; mount the
same `/app/state` volume and the same `license.bin`. Policy packs authored through ingest live under
your state/packs volume, so **persist that volume** and your custom policies survive replacement.

---

See also: **[Getting Started](getting-started.md)** · **[API Reference](api-reference.md)** ·
**[Licensing](licensing.md)**.
