kyvvu serve¶
What you'll learn: How to run a local policy evaluation server for non-Python agents.
Usage¶
Usage: kyvvu serve [OPTIONS]
Start one local policy sidecar per agent; trust local callers.
╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮
│ --host <str> Bind address [default: 127.0.0.1] │
│ --port <int> Bind port [default: 8080] │
│ --profile <path> Agent profile YAML │
│ --api-url <str> Kyvvu platform API URL │
│ --api-key <str> Bearer API key │
│ --agent-key <str> Agent key for policy fetch │
│ --policy-file <str> Manifest YAML to enforce locally (repeatable). No credentials │
│ needed. │
│ --baseline Enforce the bundled frozen baseline set. No credentials │
│ needed. │
│ --allow-ungoverned Start even when no policies are in effect. │
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
--profile agent.yaml holds the AgentProfile fields (name, purpose, owner_id, declared_tools and the rest; purpose is required). With the platform as policy source the server registers that agent at startup and prints its agent_id; with a local source it evaluates the profile against the local registration policies without contacting the platform.
What it does¶
Starts a local HTTP server that wraps the kyvvu-engine. Run one server per agent. It trusts local callers: its routes do not authenticate the agent_id a caller claims, so keep it bound to 127.0.0.1 (the default) or behind your own network controls. Non-Python agents (JavaScript, Go, Rust, etc.) can call this server to evaluate steps, record history, and end tasks — same engine, same policies, same sub-millisecond evaluation.
Two policy sources; only one needs an account¶
Credentials are needed only when policies are actually fetched. Run one of these alternatives at a time; each command keeps serving until stopped.
# Offline: no account, no API key, no network.
kyvvu serve --baseline
kyvvu serve --policy-file manifests/security/owasp-agentic-default.yaml
kyvvu serve --policy-file owasp.yaml --policy-file my-extras.yaml
kyvvu serve — policy evaluation server
Policies: bundled frozen baseline (local, no credentials)
Endpoints: http://127.0.0.1:8080/evaluate, /record, /end_task, /register_agent, /health
GET /health then reports "source": "local", "governed": true, the policy count and "load_state": "enforcing". Example snapshot just after the first load; timestamps, instance IDs and remaining TTL vary:
{
"loaded": true,
"last_success": null,
"last_attempt": "2026-10-16T18:38:59.480041Z",
"policy_count": 15,
"stale": false,
"source": "local",
"last_fetch_at": null,
"last_fetch_succeeded": false,
"instance_id": "898dc674-6b88-4ac2-837f-fb9a94b6e4c8",
"ttl_remaining_seconds": 300.0,
"load_state": "enforcing",
"policies_offered": 15,
"registration_blocked_by": [],
"stale_cause": null,
"client_upgrade": null,
"agent_id": null,
"applicable_policy_count": null,
"registration_blocked": false,
"governed": true
}
stale_cause and client_upgrade are null unless the set is stale. A server
the platform refused as too old keeps enforcing its cached set and reports
"load_state": "stale_cache", "stale_cause": "client_upgrade_required" and,
under client_upgrade, the platform's answer with the upgrade command;
REST API, "Health check" has that
response in full.
A local source clears the credentials for that process, so --baseline in a shell that exports KV_API_KEY runs the bundled set rather than quietly fetching a different one. An invalid or missing manifest refuses to start, with every problem listed: a server that boots ungoverned on a typo is worse than one that does not boot.
The server exits non-zero when the initial policy load has no policies in effect. This includes a valid manifest with policies: [] (none_configured) and an unreachable platform with no cached policies (unavailable); the error names the load state. Use --allow-ungoverned to start deliberately in either state. /health then answers 200 with "governed": false and the load state. A validated cached set remains governed if the platform becomes unreachable (stale_cache). An invalid manifest still refuses to start, even with --allow-ungoverned.
Precedence is flags > environment > .env:
--policy-file/--baselinewin over an exportedKV_API_KEY.--api-key/--agent-keywin over an exportedKV_POLICY_FILE.- Passing local and API flags together is refused rather than resolved, because silently picking a winner is how a server ends up enforcing something other than what was asked for.
- With nothing explicit, an exported
KV_POLICY_FILEis honoured — unless the API is also fully configured, in which case the API wins, matching what an embeddedKyvvuRunnerwith the same environment would do.
Prerequisites¶
kyvvu serve ships in kyvvu-cli, which installs the engine server and its fastapi and uvicorn dependencies:
uv tool install kyvvu-cli
Starting the server against the platform¶
$ export KV_API_URL=https://platform.kyvvu.com
$ export KV_API_KEY=KvKey-...
$ export KV_AGENT_KEY=my-agent
$ kyvvu serve
kyvvu serve — policy evaluation server
API: https://platform.kyvvu.com
Agent: my-agent
Agent id: agent_3f9c2a7b1d4e6f80 (send this as agent_id)
Endpoints: http://127.0.0.1:8080/evaluate, /record, /end_task, /register_agent, /health
Agent id is the platform agent_id the policies were fetched for, the one
POST /api/v1/agents returned for this agent_key. Send it as agent_id
(and context.agent_id) on every call: it is the only one this server accepts.
Any other, your own name for the agent or the agent_key included, is refused
with HTTP 409 and a detail naming both ids, because none of the agent's own
policies would apply to it.
GET /health reports it as agent_id, beside applicable_policy_count, the
number of loaded policies that apply to it. The line is printed whenever the
server knows the id at start: from the fetch, or from the policy cache it fell
back on when the platform was unreachable.
Or with explicit flags:
kyvvu serve --host 0.0.0.0 --port 9090 \
--api-url https://platform.kyvvu.com \
--api-key KvKey-... \
--agent-key my-agent
Endpoints¶
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
Liveness probe: returns the full policy status, including governed, source, load_state, stale_cause, client_upgrade, policy count, policies_offered, registration_blocked and registration_blocked_by, the governed agent_id and its applicable_policy_count, last fetch time and TTL remaining. Returns 200 once the server is up. |
POST |
/evaluate |
Preflight evaluation of an intended behaviour. Returns {action, risk_score, policies, action_id, blocked, recorded_seq}. A blocked step.* is recorded by /evaluate itself, and recorded_seq is then set: do not /record it again. |
POST |
/record |
Record a completed step. Returns {seq, task_id}. |
POST |
/end_task |
Close a task — evict history and flush logs. Requires task_id and agent_id. Returns {status, task_id}. |
POST |
/register_agent |
Evaluate agent-registration policies against the agent_data you send. Registers nothing with the platform, but the server holds the result: a blocked registration blocks every step.* until a later /register_agent passes (details). |
Request bodies, the fields each one requires, and real responses for every endpoint are in REST API (Non-Python). Three things that catch first-time callers:
intended.task_idis the only task identity, and a task opens with itstask.start.context.task_idis ignored. Record thetask.startfirst: any other/evaluateor/recordfor a task the server has not seen open is a 409, as are a secondtask.start, an/end_taskfor an unopened task and any write after/end_task. Path state is kept pertask_id, in the server's memory, and is empty after a restart. See Task identity and state.- One server serves one agent. With a local source the first
agent_idbinds it; another is a 409. - With the platform as the policy source, send the platform
agent_id(agent_…, from registration, andagent_idin/health), not a name of your own. Policies from the platform carry that id, so it is the one agent the server governs, and any otheragent_idis a 409 naming both, on every endpoint.--baselineand--policy-fileare not affected. context.environmentdefaults todevelopment(the Python client defaults toproduction), so send it.contextis strict: an unknown key, or anenvironmentorrisk_classificationthat is not a valid enum value, is a 422 naming it. Onlycontext.agent_idis required; everything else incontextis optional.- A policy decision is always HTTP 200. Read
action(allow,warn,block) andblockedin the body. A 422 is a malformed request, and its message for a badkindandverbpair names the verbs that action kind takes.
Configuration¶
All KV_* environment variables work identically to KyvvuRunner:
KV_POLICY_FILE— manifest YAML path(s), comma-separated: the credential-free policy sourceKV_API_URL— platform API URLKV_API_KEY— bearer API keyKV_AGENT_KEY— agent key for policy fetchKV_LOG_LOCATION— trace flush destination (URL,stdout, file path,none, orauto— the default, which follows the policy source)
Warning
autochanges where a credentialedkyvvu servewrites its trace. WithKV_API_URL,KV_API_KEYandKV_AGENT_KEYset and no--policy-file, the policy source is the platform, soautosends the trace there too — a sidecar that was piping this container's stdout goes quiet. SetKV_LOG_LOCATION=stdoutto keep the old behaviour. Offline mode (--policy-file) is unaffected: it resolves tostdout, even in a shell exportingKV_API_KEY. -KV_LOG_FORMAT— trace format (kv,json, orotlp) -KV_INCIDENT_LOCATION— incident destination; unset = inherit the trace sink (…/api/v1/incidentsforkv) -KV_INCIDENT_FORMAT— incident format; unset = inheritKV_LOG_FORMAT-KV_POLICY_TTL_SECONDS— policy cache TTL
The server also reads a .env in the directory you start it from, which fills in only variables that are not already set; see where settings are read. A relative --policy-file is relative to that directory too. With no --api-url and no KV_API_URL, the API URL is the one your kyvvu auth login saved in ~/.kyvvu/config.toml; the API key never comes from that file.
Next steps¶
- kyvvu try — the same offline enforcement, without the HTTP server
- REST API (Non-Python) — integration guide with curl examples
- Engine settings — all environment variables
- Architecture — why in-process evaluation is preferred over remote