# Runtime mode
`openlatch-provider listen` is the long-lived daemon that receives
HMAC-signed Standard Webhooks v1 from `openlatch-platform`, verifies
them, proxies the events to your localhost-hosted detection tool, and
returns HMAC-signed verdicts.
## Lifecycle per event
```
1. POST /v1/event arrives (axum + tower-http RequestBodyLimit 1 MB)
2. Read raw body bytes (Bytes — no parse yet)
3. Verify webhook-signature header (HMAC-SHA256 over <id>.<ts>.<body>)
4. Skew check on webhook-timestamp (±5 min)
5. Replay-cache lookup on webhook-id (LRU 1000, 5 min TTL)
└── hit -> return cached signed verdict (idempotent)
6. Resolve X-OpenLatch-Binding-Id -> LocalRoute (HashMap snapshot)
7. Parse body as JSON (only after auth verified)
8. POST event to localhost tool (deadline-bounded, no redirects)
9. Validate verdict size (≤ 250 KB cap)
10. HMAC-sign outbound headers (reusing the same whsec_live_… secret)
11. Insert into replay cache (so retries are idempotent)
12. Append audit-log line (mpsc channel + bg writer task)
13. Return signed response (200 + the 3 webhook-* headers)
```
Failure modes short-circuit early; a malformed signature MUST stop
before `serde_json::from_slice`.
## Listen flags
| Flag | Purpose |
|---|---|
| `--port 8443` | HTTPS port. Use `0` for OS-assigned in tests. |
| `--bind-addr 0.0.0.0` | Bind address. Default 0.0.0.0; use `127.0.0.1` behind a reverse proxy. |
| `--manifest <path>` | Override the active profile's manifest. |
| `--cert <pem> --key <pem>` | Direct TLS termination (not yet wired in v0.1; see `--no-tls`). |
| `--no-tls` | Plain TCP. Use behind a TLS-terminating reverse proxy. |
| `--no-watch` | Disable cross-platform manifest file-watcher. |
| `--offline` | Offline / manifest-only mode (self-host & dev). No platform round-trip. See below. Also via `OPENLATCH_PROVIDER_OFFLINE=1`. |
| `--admin-port <p>` | Bind the bearer-auth admin endpoint on `127.0.0.1:<p>`. |
| `--log-retention-days <n>` | Audit log rotation (default 14). |
## Offline / manifest-only mode
`openlatch-provider listen --offline` (or `OPENLATCH_PROVIDER_OFFLINE=1`)
boots the runtime **without any platform round-trip** — the path used to
self-host the OpenLatch built-in security tools (openlatch-sectools) or to
iterate locally without onboarding bindings.
What changes versus the online path:
- **No `/api/v1/editor/bindings` fetch.** No editor token / login is
required. Routes are synthesized from the v2 manifest alone: every
binding's tool's declared capability `threat_category` maps to that
binding's `local_endpoint`.
- **One secret, from the environment.** `OPENLATCH_BINDING_SECRET`
(Standard-Webhooks `whsec_<base64>` form — the **same** value the
platform signs with, e.g. `OPENLATCH_BUILTIN_BINDING_SECRET`) verifies
**every** inbound request. There is no keyring/file per-binding secret
store and no `bindings rotate-secret` step.
- **Routing key is the request `category`, not the binding id.** The
platform's single built-in binding sends every threat category to one
ingress with a binding UUID that cannot be known offline, so the inbound
`X-OpenLatch-Binding-Id` is informational only — the request body's
`category` selects the manifest tool. An unknown category is a clean
`OL-4222`; a missing platform side is **never** an error (the whole
point of offline mode).
- **Hot-reload is disabled.** The route table is immutable for the process
lifetime — SIGHUP / file-watch / `POST /v1/admin/reload` are not wired.
Restart to pick up manifest edits.
Startup still fails loudly (`OL-4222`) when `OPENLATCH_BINDING_SECRET` is
absent or the manifest resolves zero routable categories.
## Reload triggers
The daemon's route table can be refreshed without restart **(online mode
only — offline has an immutable manifest-derived table)**:
- **SIGHUP** (Unix only) — `kill -HUP $PID` or `systemctl reload`.
- **Manifest file change** — cross-platform via the `notify` watcher.
- **`POST /v1/admin/reload`** — bearer-auth admin endpoint, gated on
`--admin-port`.
Each path runs the same internal `reload_into(...)` function: refresh
live bindings from the platform (best-effort), build a new
`RouteTable`, swap atomically.
## Auto-update
The runtime daemon includes the auto-update worker (P3.T2c). Default
behaviour:
- Spawns at startup unless a kill-switch fires (`OPENLATCH_NO_AUTO_UPDATE=1`,
`OPENLATCH_PROVIDER_AUTO_UPDATE=false`, CI env, or `cargo install`).
- Polls the npm registry every 6 h (1 h on critical, 5 min on defer-recheck).
- Activity-aware: 60 s quiet window default + `events_in_flight == 0`,
24 h hard cap, `severity=critical` bypasses deferral.
- Apply pipeline: download -> SRI verify -> tar extract (hardened) ->
multi-key minisign verify -> sanity-check `--version` ->
`self_replace::self_replace` -> sentinel write -> drain -> re-exec.
Manual control:
```bash
openlatch-provider update --check # JSON status: current/latest/severity/min_supported
openlatch-provider update --apply --yes
openlatch-provider update --apply --yes --force-cargo # bypass cargo-install gate
```
## Telemetry
Two separate consent subsystems:
- **PostHog usage telemetry**: opt-in. Disabled by default; toggle via
`openlatch-provider config set telemetry true`. CI environments
auto-disable. `DO_NOT_TRACK=1` is the cross-tool kill switch.
- **Sentry crash reports**: default-on. Captures Rust panic
backtraces only, scrubbed of stack-locals + env-var values + tokens.
Disable with `SENTRY_DISABLED=1` or `[crashreport] enabled = false`
in `~/.openlatch/provider/config.toml`.
## Deploy patterns
- **systemd**: `docs/deploy/systemd.example` (hardened unit file).
- **Docker**: `docs/deploy/docker.md` (multi-arch image at
`ghcr.io/openlatch/provider`).
- **Caddy reverse proxy**: `docs/deploy/caddy-tls-termination.md`.
## Health
`GET /v1/health` returns:
```json
{
"status": "ok",
"uptime_secs": 1234,
"events_processed": 5678,
"events_failed": 2,
"binding_count": 3
}
```
No auth required. Use as your liveness + readiness probe.