Skip to content

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 / --baseline win over an exported KV_API_KEY.
  • --api-key / --agent-key win over an exported KV_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_FILE is honoured — unless the API is also fully configured, in which case the API wins, matching what an embedded KyvvuRunner with 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_id is the only task identity, and a task opens with its task.start. context.task_id is ignored. Record the task.start first: any other /evaluate or /record for a task the server has not seen open is a 409, as are a second task.start, an /end_task for an unopened task and any write after /end_task. Path state is kept per task_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_id binds it; another is a 409.
  • With the platform as the policy source, send the platform agent_id (agent_…, from registration, and agent_id in /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 other agent_id is a 409 naming both, on every endpoint. --baseline and --policy-file are not affected.
  • context.environment defaults to development (the Python client defaults to production), so send it. context is strict: an unknown key, or an environment or risk_classification that is not a valid enum value, is a 422 naming it. Only context.agent_id is required; everything else in context is optional.
  • A policy decision is always HTTP 200. Read action (allow, warn, block) and blocked in the body. A 422 is a malformed request, and its message for a bad kind and verb pair 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 source
  • KV_API_URL — platform API URL
  • KV_API_KEY — bearer API key
  • KV_AGENT_KEY — agent key for policy fetch
  • KV_LOG_LOCATION — trace flush destination (URL, stdout, file path, none, or auto — the default, which follows the policy source)

Warning

auto changes where a credentialed kyvvu serve writes its trace. With KV_API_URL, KV_API_KEY and KV_AGENT_KEY set and no --policy-file, the policy source is the platform, so auto sends the trace there too — a sidecar that was piping this container's stdout goes quiet. Set KV_LOG_LOCATION=stdout to keep the old behaviour. Offline mode (--policy-file) is unaffected: it resolves to stdout, even in a shell exporting KV_API_KEY. - KV_LOG_FORMAT — trace format (kv, json, or otlp) - KV_INCIDENT_LOCATION — incident destination; unset = inherit the trace sink (…/api/v1/incidents for kv) - KV_INCIDENT_FORMAT — incident format; unset = inherit KV_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