The KAI-C Orchestrator
KAI-C, the OpenNVR middleware that registers AI adapters, enforces sovereignty, and writes the audit trail every inference lands in.
KAI-C (Kavach AI Connector) is the middleware between the NVR backend and the AI adapters. The backend never calls an adapter directly. Every inference goes through KAI-C, which is what makes the audit trail complete rather than best-effort.
It lives in the open-nvr repository at kai-c/, and ships inside the
opennvr-core container — there is no separate service to install. It
listens on port 8100:
Frontend → Backend (8000) → KAI-C (8100) → AI adapter (9100)
Why a middleware at all
A thin proxy would be pointless. KAI-C earns its place by owning four things the core deliberately does not:
- The adapter registry — on registration and every 60 seconds after,
KAI-C polls each adapter’s
/capabilities, caching its tasks, model fingerprint and health. - Sovereignty enforcement — under the default
local_onlypolicy it refuses any adapter declaring non-emptypermissions.network_egress. Underfederatedit refuses wildcard egress. This is re-checked on every poll, so an adapter that becomes cloud-backed at runtime is de-registered rather than grandfathered in. - The audit log — an append-only JSONL store. Every registration, inference, refusal and fingerprint mismatch is recorded and queryable.
- Correlation IDs — every request gets an
X-Correlation-Id, minted if absent, threaded to the adapter, echoed back, and stamped on every audit event. One id joins the logs across KAI-C, the adapter and whatever consumed the result.
Drift handling
Registering an adapter is a trust decision, so KAI-C watches for that trust changing underneath you. What happens depends on what drifted:
| What changed | What KAI-C does |
|---|---|
model.fingerprint |
Records adapter.fingerprint_mismatch, keeps serving — the operator decides. |
model.version |
Records adapter.capability_drift, keeps serving. |
permissions.* gains a permission |
Blocking. The adapter flips to pending and stops serving until an operator re-approves it. |
permissions.network_egress violates the policy |
De-registers, and records inference.refused_sovereignty. |
endpoints.* |
Audited, no action. |
scheduling.* |
Applied silently. |
A model quietly swapped behind an endpoint you already approved is the attack this exists to make visible.
The v1 API
| Endpoint | Purpose |
|---|---|
POST /api/v1/adapters/register |
Register an adapter URL — polls /capabilities, runs the sovereignty check, stores it. |
DELETE /api/v1/adapters/{name} |
Deregister. |
GET /api/v1/adapters |
Adapter summaries for the UI. |
GET /api/v1/ai/capabilities |
Aggregated capabilities across every adapter, in one call. |
POST /api/v1/adapters/refresh |
Force a /capabilities + /health refresh. |
GET /api/v1/audit |
Query the audit log — filter by adapter, event type, camera, or time. |
POST /api/v1/infer/{adapter} |
Contract-compliant inference with correlation-ID threading and audit emission. |
Audit events use a fixed vocabulary: adapter.registered,
adapter.deregistered, adapter.fingerprint_mismatch,
adapter.capability_drift, adapter.unavailable, inference.completed,
inference.failed, inference.refused_sovereignty. The log is JSONL on
disk at $KAI_C_AUDIT_LOG.
Running it outside the container
For development — debugging bounding-box mapping, say — kai-c/ runs
standalone. It is a uv project:
cd kai-c/
uv sync
python start.py # serves on http://localhost:8100
start_no_reload.py is the same thing without the reloader, which is
what you want under load testing. See kai-c/README.md for the current
behaviour and kai-c/DESIGN.md for where the architecture is going —
a control plane with capability tokens, and no proxy in the inference
data path.