> For the complete documentation index, see [llms.txt](https://docs.kyvvu.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kyvvu.com/integrations/rest-api.md).

# REST API / Local Engine

**What you'll learn:** How to integrate non-Python agents with Kyvvu using the local HTTP server (`kyvvu serve`).

***

## `kyvvu serve` (local engine)

For agents written in JavaScript, Go, Rust, or any language, `kyvvu serve` runs the engine as a local HTTP server. Your agent makes HTTP calls to evaluate steps — same engine, same sub-millisecond policies, no Python needed in your agent code.

### Start the server

```bash
pip install kyvvu
```

The server needs a policy source, and there are two kinds. **Only one of them needs an account.**

**Local — no credentials, no network:**

```bash
kyvvu serve --baseline                       # the frozen 15-policy bundled baseline
kyvvu serve --policy-file my-manifest.yaml   # your own manifest (repeatable)
```

**Platform — fetches this agent's current policy set:**

```bash
export KV_API_URL=https://platform.kyvvu.com
export KV_API_KEY=KvKey-...
export KV_AGENT_KEY=my-agent
kyvvu serve --host 127.0.0.1 --port 8080
```

A `kyvvu serve` with neither does not start: the runner refuses a configuration naming no policy source, and the server exits 1 with a message naming what is missing. That is deliberate — the complaint lands where the configuration is supplied rather than on the first governed step.

### Endpoints

| Method | Path              | Purpose                                        |
| ------ | ----------------- | ---------------------------------------------- |
| `GET`  | `/health`         | Liveness probe — returns policy status.        |
| `POST` | `/evaluate`       | Preflight evaluation of an intended behaviour. |
| `POST` | `/record`         | Record a completed step into task history.     |
| `POST` | `/end_task`       | Close a task — evict history and flush logs.   |
| `POST` | `/register_agent` | Evaluate agent-registration policies.          |

### Health check

```bash
curl http://127.0.0.1:8080/health
```

```json
{
  "loaded": true,
  "policy_count": 15,
  "policies_offered": 15,
  "load_state": "enforcing",
  "dropped": [],
  "stale": false,
  "source": "api",
  "last_fetch_at": "2026-04-29T10:00:00+00:00",
  "last_fetch_succeeded": true,
  "instance_id": "worker-3-a8f92",
  "ttl_remaining_seconds": 280.5
}
```

**Read `load_state`, not `policy_count`.** A count of zero cannot tell three different failures apart, and all three allow every step:

| `load_state`      | Meaning                                                          | Governed? |
| ----------------- | ---------------------------------------------------------------- | --------- |
| `enforcing`       | Policies loaded and in effect                                    | yes       |
| `stale_cache`     | Serving a cache past its TTL after a failed fetch                | yes       |
| `none_configured` | The source has no policies — nothing assigned                    | **no**    |
| `unavailable`     | The source could not be reached                                  | **no**    |
| `all_dropped`     | Policies were offered and every one was rejected — see `dropped` | **no**    |

`policies_offered` against `policy_count` tells you how much of the offered set survived: `policies_offered: 15, policy_count: 12` means three policies you wrote are inert, and `dropped` names them with reasons.

### Evaluate a step

```bash
curl -X POST http://127.0.0.1:8080/evaluate \
  -H "Content-Type: application/json" \
  -d '{
    "intended": {
      "agent_id": "agent-123",
      "task_id": "task-abc",
      "step_type": "step.model",
      "verb": "POST",
      "step_name": "chat_gpt-4o",
      "input": {"user_message": "Hello"}
    },
    "context": {
      "agent_id": "agent-123",
      "task_id": "task-abc",
      "environment": "production",
      "risk_classification": "limited"
    }
  }'
```

Response:

```json
{
  "action": "allow",
  "risk_score": 0.0,
  "policies": [
    {
      "policy_id": 1,
      "name": "pii_in_request",
      "severity": "critical",
      "violated": false,
      "violation_details": null
    }
  ],
  "blocked": false
}
```

When a policy blocks, `blocked` is `true` and `action` is `"block"`. The server always returns HTTP 200 for policy decisions — read the `blocked` field to decide whether to proceed.

### Record a completed step

```bash
curl -X POST http://127.0.0.1:8080/record \
  -H "Content-Type: application/json" \
  -d '{
    "step": {
      "agent_id": "agent-123",
      "task_id": "task-abc",
      "step_type": "step.model",
      "verb": "POST",
      "step_name": "chat_gpt-4o",
      "input": {"user_message": "Hello"},
      "output": {"response": "Hi there!"}
    }
  }'
```

> `/record` nests the Behavior under a `step` key. `/evaluate` nests it under `intended` and requires a sibling `context` object.

> **Behavior objects are strict.** A Behavior in a request body accepts only the fields the schema defines: `agent_id`, `task_id`, `timestamp`, `step`, `step_type`, `verb`, `step_name`, `input`, `output`, `properties`, and `meta`. Any other key returns HTTP 422 identifying the field the server rejected.

Response: `{"step": 1, "task_id": "task-abc"}`

### End a task

```bash
curl -X POST http://127.0.0.1:8080/end_task \
  -H "Content-Type: application/json" \
  -d '{"task_id": "task-abc"}'
```

Response: `{"status": "ok", "task_id": "task-abc"}`

### Integration pattern

The typical flow for a non-Python agent:

```
1. POST /evaluate  → check if step is allowed
2. If blocked=false: execute the step
3. POST /record    → record the completed step with output
4. Repeat for each step
5. POST /end_task  → flush logs and clean up
```

***

## Next steps

* [kyvvu serve](/cli-reference/serve.md) — CLI reference for the serve command
* [Configuration Reference](/deployment/configuration.md) — all environment variables
* [Architecture](/core-concepts/architecture.md) — why in-process evaluation is preferred
