# Introduction

**The Agent Security Kernel (ASK) for AI Agents. Every agent has to ASK before it acts.**

Kyvvu is an Agent Security Kernel (ASK): an in-process security layer that evaluates each action against the agent's full task path — before the step runs — to stop data leakage and destructive actions. Runtime governance follows. It decides **allow**, **warn**, or **block** in sub-millisecond time, directly inside your agent process. No proxy. No gateway. No perceptible latency.

***

## The first 5 minutes

```bash
pip install kyvvu

kyvvu register
# → creates your account, returns an API key

kyvvu init my-agent
# → scaffolds a demo agent project

cd my-agent
pip install -r requirements.txt
export KV_API_KEY=KvKey-...
python agent.py
```

The demo agent runs three decorated steps (a model call, a resource read, and a code execution). Without policies assigned, all steps pass. To see enforcement in action:

```bash
kyvvu list-manifests
kyvvu assign-manifest --agent-id <id> --repo-id 1 --manifest owasp_agentic_default.yaml
python agent.py
```

Now the OWASP policy "Code execution requires a preceding gate" blocks the `step.exec`:

```
Policy blocked this step: ...
   Risk score: 1.00
   Action:     block
   * Code execution requires a preceding gate (critical)
```

That's runtime policy enforcement — policies defined in a YAML manifest, assigned to your agent, evaluated against the full task history.

For why this architecture matters, see the blog post: [The Hot Path Tax](https://kyvvu.com/blog/2026/04/29/hot-path-tax/).

***

## Architecture

```
Your Agent Code
  |  @kv.step / LangChain handler / REST API
  v
kyvvu SDK (translates events -> Behaviors)
  |  template.match() -> deep_merge -> Behavior
  v
kyvvu-engine (in-process, sub-ms policy evaluation)
  |  evaluate() -> allow / warn / block
  |  record() -> append to task history
  |  end_task() -> flush behavioral trace
  v
Kyvvu Platform API (platform.kyvvu.com)
  |  policy storage, incident management, audit trail
  v
Dashboard (platform.kyvvu.com)
  |  policy config, incident triage, reports
```

The engine runs **in your process**. Policy evaluation is pure CPU — no network calls, no database queries, no I/O. Policies are fetched in the background and cached. Log flushing happens asynchronously on task completion.

***

## What Kyvvu does

1. **Registration enforcement** — agents must declare their purpose, tools, and risk classification before they can start. Policies validate these declarations at startup.
2. **Runtime policy evaluation** — every atomic step your agent takes is evaluated against loaded policies *before* execution. Decisions depend on the full ordered history of the current task ("policies on paths").
3. **Behavioral trace logging** — completed steps are recorded into an audit trail, flushed to the platform API on task completion. The trace is a structured JSON record of everything the agent did.
4. **Incident management** — policy violations generate incidents that surface in the dashboard for triage, acknowledgment, and resolution.

***

## Quick links

| Resource      | URL                                                                                     |
| ------------- | --------------------------------------------------------------------------------------- |
| Documentation | [docs.kyvvu.com](https://docs.kyvvu.com)                                                |
| Platform      | [platform.kyvvu.com](https://platform.kyvvu.com)                                        |
| GitHub        | [github.com/Kyvvu/platform](https://github.com/Kyvvu/platform)                          |
| PyPI          | [pypi.org/project/kyvvu](https://pypi.org/project/kyvvu)                                |
| Paper         | [Runtime Governance for AI Agents: Policies on Paths](https://arxiv.org/abs/2603.16586) |
| Jobs          | [kyvvu.com/join](https://kyvvu.com/join/)                                               |

***

## Next steps

* [Installation](/getting-started/installation) — install the SDK and set up your account
* [Your First Agent](/getting-started/first-agent) — walk through `kyvvu init` step by step
* [Architecture](/core-concepts/architecture) — understand the three-package split and why the engine is in-process
* [Creating Policies](/policy-authoring/creating) — author your first custom policy


# Installation

**What you'll learn:** How to install the Kyvvu SDK, create an account, and verify your setup.

***

## Install the SDK

```bash
pip install kyvvu
```

This installs the `kyvvu` Python package, which includes:

* The **SDK** (`kyvvu`) — decorator and callback integrations for your agent code
* The **engine** (`kyvvu-engine`) — in-process policy evaluation library
* The **CLI** (`kyvvu` command) — account management, project scaffolding, local server

## Create an account

```bash
kyvvu register
```

You'll be prompted for an email, display name, and password. On success, you'll receive an API key:

```
Registering at https://platform.kyvvu.com

Email: you@company.com
Display name: Your Name
Password:
Confirm password:

✓ Account created and verified.

Your API key (save this — it will not be shown again):

    KvKey-abc123...

Set it in your environment:

    export KV_API_KEY=KvKey-abc123...

Next steps:
    kyvvu init my-agent
```

Save the API key — it authenticates your agents with the Kyvvu platform.

{% hint style="info" %}
**Instant access:** Registration is immediate — no email verification step. Your account is active as soon as you register. Organization founders get full access instantly; team members joining an existing org need admin verification.
{% endhint %}

## Verify

```bash
kyvvu whoami
```

```
Email:         you@company.com
API URL:       https://platform.kyvvu.com
Verification:  ✓ verified
Log location:  stdout
Incidents:     (inherits log location: stdout)
```

## Environment variables

Set `KV_API_KEY` in your shell profile or `.env` file:

```bash
export KV_API_KEY=KvKey-...
```

For self-hosted deployments, also set the API URL:

```bash
export KV_API_URL=https://your-kyvvu-instance.com
```

## Licensing note

The SDK itself is licensed under Apache 2.0. It depends on `kyvvu-engine`, which is licensed under the Business Source License 1.1 (BSL 1.1). Production use of the engine requires a Kyvvu commercial subscription. See the [Licensing](/reference/licensing) page for details.

***

## Next steps

* [Your First Agent](/getting-started/first-agent) — create and run a demo agent with `kyvvu init`
* [Architecture](/core-concepts/architecture) — understand how the SDK, engine, and platform fit together


# Your First Agent

**What you'll learn:** How `kyvvu init` works, what the demo agent does, and how to read the policy evaluation output.

***

## Scaffold a project

```bash
kyvvu init my-agent
```

This scaffolds project files — `agent.py`, `requirements.txt`, `.env.example`, `.gitignore`, and `README.md`. No authentication is required.

The output looks like:

```
✓ Created my-agent/

Next steps:

    cd my-agent

    # Install requirements:
    pip install -r requirements.txt

    # Set your key:
    # (run `kyvvu register` if you have none)
    cp .env.example .env       # add your KV_API_KEY
    # or: export KV_API_KEY=your-key

    # Run the agent:
    python agent.py

To assign security policies, go to the dashboard:
    Manifests page → select a manifest → assign to your agent
Or use the CLI:
    kyvvu list-manifests
    kyvvu assign-manifest --agent-id <id> --repo-id <id> --manifest <path>
```

{% hint style="info" %}
**Caching:** `register_agent()` results are cached locally at `~/.kyvvu/registrations/`. If you call it on every startup with the same parameters, the SDK reuses the cached registration without a network call. You can safely call `register_agent()` every time your agent starts.
{% endhint %}

{% hint style="info" %}
**Data minimization:** By default, only your agent's opaque ID and operational fields (risk classification, environment, declared tools) are stored in Kyvvu's cloud. Descriptive metadata (name, purpose, owner) is not persisted server-side. Pass `store_metadata=True` to opt in to storing metadata on the platform.
{% endhint %}

## Assign security policies

Policies are managed via **manifests** — YAML files stored in a GitHub repository. To assign policies to your agent:

1. Go to the **Manifests** page in the dashboard
2. Connect a repository containing manifest files (or use the default Kyvvu/manifests repo)
3. Click **Assign** on a manifest and select your agent

Or use the CLI:

```bash
kyvvu list-manifests
kyvvu assign-manifest --agent-id <id> --repo-id <id> --manifest manifests/security/owasp-agentic-default.yaml
```

See [Manifests](/policy-authoring/templates) for details on manifest structure and available policy sets.

## Run the demo agent

```bash
cd my-agent
pip install -r requirements.txt
export KV_API_KEY=KvKey-...
python agent.py
```

The full `agent.py` source:

```python
from kyvvu import Kyvvu, KyvvuBlockedError, RiskClassification, StepType, Verb
import os, sys

# --- Constructor ---
# api_url: Kyvvu platform URL (defaults to https://platform.kyvvu.com)
# api_key: your API key from `kyvvu register` (reads KV_API_KEY env var)
# agent_key: stable identifier for this agent (used for policy matching)
# risk_classification: EU AI Act risk tier (MINIMAL, LIMITED, or HIGH)
kv = Kyvvu(
    api_url=os.environ.get("KV_API_URL", "https://platform.kyvvu.com"),
    api_key=os.environ["KV_API_KEY"],
    agent_key="my-agent",
    risk_classification=RiskClassification.MINIMAL,
)

# --- Registration ---
# Registers the agent with the platform. Results are cached locally
# at ~/.kyvvu/registrations/ so repeat calls skip the network request.
# By default, only the opaque agent ID and operational fields are stored
# server-side (name, purpose, owner are NOT persisted unless you pass
# store_metadata=True).
kv.register_agent(
    name="Hello Kyvvu",
    purpose="Demonstration agent — shows behavioral trace and policy evaluation",
    declared_tools=["call_llm", "fetch_user_data", "run_script"],
)

# --- Decorated steps ---
# @kv.step wraps each function in evaluate → execute → record:
#   step_model  = LLM / model call
#   step_resource = external data fetch
#   step_exec   = code execution (triggers OWASP gate requirement)
# Verb describes the HTTP-like action (POST, GET, etc.)

@kv.step(StepType.step_model, Verb.POST)
def call_llm(prompt: str) -> str:
    """Mocked model call — returns a canned response."""
    return f"(mocked response to: {prompt!r})"

@kv.step(StepType.step_resource, Verb.GET)
def fetch_user_data(user_id: str) -> dict:
    """Mocked external resource call."""
    return {"user_id": user_id, "name": "Alice", "tier": "free"}

@kv.step(StepType.step_exec)
def run_script(code: str) -> str:
    """Mocked code execution — intentionally blocked by OWASP policy."""
    return f"(would execute: {code!r})"

# --- Task lifecycle ---
# start_task() begins a new auditable task run.
# end_task() flushes the audit trail. error_task() records a failure.
# KyvvuBlockedError is raised when a policy blocks a step — the agent
# should handle it gracefully (log, skip, or escalate).
if __name__ == "__main__":
    try:
        kv.start_task()
        user = fetch_user_data("user_123")
        answer = call_llm(f"User {user['name']} asks: What's the weather?")
        run_script(f"save_response('{answer}')")  # blocked by OWASP policy
    except KyvvuBlockedError as exc:
        print(f"\n⛔ Policy blocked this step: {exc}")
        print(f"   Risk score: {exc.result.risk_score:.2f}")
        print(f"   Action:     {exc.result.action.value}")
        for p in exc.result.policies:
            if p.violated:
                print(f"   • {p.name} ({p.severity})")
        try:
            kv.error_task(error=exc)
        except Exception:
            pass
        sys.exit(1)
    except Exception as exc:
        try:
            kv.error_task(error=exc)
        except Exception:
            pass
        raise
    else:
        kv.end_task()
```

It starts a task, calls all three steps, and the third one gets blocked:

```
⛔ Policy blocked this step: ...
   Risk score: 1.00
   Action:     block
   * Code execution requires a preceding gate (critical)

This is Kyvvu working as intended — the OWASP security policies
flagged an action that requires oversight. In a real agent, you'd
add a step.gate (human approval) before the blocked action.
```

## What happened

The `@kv.step` decorator wraps each function call in a three-phase cycle:

1. **Evaluate** — the engine checks the intended behaviour against all applicable policies. It reads the task's history to make path-dependent decisions.
2. **Execute** — if the evaluation returns `allow` or `warn`, the function runs.
3. **Record** — the completed step (with output) is appended to the task's history for future evaluations.

Steps 1 and 2 passed for `call_llm` and `fetch_user_data`. When `run_script` was evaluated:

* The engine identified it as `step.exec` (code execution).
* The OWASP policy "Code execution requires a preceding gate" checked whether a `step.gate` existed earlier in the task history.
* No gate was found. The policy has severity `critical`, so the engine returned `block`.
* The decorator raised `KyvvuBlockedError` instead of executing the function.

## Inspect the policies

```bash
kyvvu list-policies
```

This shows policies assigned to your agent via manifests. If no manifest has been assigned yet, the list will be empty.

```
 NAME                                        SCOPE               RULE_TYPE              SEVERITY  SOURCE                       ENABLED
 Agent must declare a substantive purpose    agent_registration  field_matches_regex    high      owasp_agentic_default.yaml   ✓
 Agent must declare a tool allowlist         agent_registration  field_not_empty        high      owasp_agentic_default.yaml   ✓
 Agent must declare a valid risk class.      agent_registration  field_in_list          critical  owasp_agentic_default.yaml   ✓
 Tool calls must be in the declared allow.   step_execution      step_name_in_allowlist critical  owasp_agentic_default.yaml   ✓
 Code execution requires a preceding gate    step_execution      step_requires_gate     critical  owasp_agentic_default.yaml   ✓
 Destructive resource ops require a gate     step_execution      step_requires_gate     critical  owasp_agentic_default.yaml   ✓
 External content taint requires fresh gate  step_execution      not                    critical  owasp_agentic_default.yaml   ✓
 Bound resource calls per task (max 50)      step_execution      execution_max_steps    high      owasp_agentic_default.yaml   ✓
```

Each policy is an instance of a **rule** with specific **parameters**. For example, "Code execution requires a preceding gate" uses the `step_requires_gate` rule with `target_step_types: ["step.exec"]`. The same rule backs "Destructive resource ops require a gate" with different parameters (`target_step_types: ["step.resource"]`, `target_verb: "DELETE"`).

For the full details of each OWASP policy, see [OWASP Default Manifest](/policy-authoring/owasp-default).

***

## Next steps

* [Understanding the Output](/getting-started/understanding-output) — learn what each field in the behavioral trace means
* [OWASP Default Template](/policy-authoring/owasp-default) — the 8 default policies explained in detail
* [Python Decorator](/integrations/decorator) — full reference for `@kv.step`


# Understanding the Output

**What you'll learn:** What a behavioral trace is, what each field means, and the three kinds of "logging" in Kyvvu.

***

## The behavioral trace

Every step your agent takes produces a structured JSON record — a **Behavior**. The complete sequence of Behaviors within a task is the **behavioral trace**. This is the audit trail Kyvvu produces.

A single Behavior looks like:

```json
{
  "agent_id": "ag_abc123",
  "task_id": "task-def456",
  "scope": "step",
  "step_type": "step.model",
  "verb": "POST",
  "step_name": "call_llm",
  "input": {"prompt": "What's the weather?"},
  "output": {"response": "(mocked response)"},
  "properties": {"model": {"name": "gpt-4o"}},
  "meta": {
    "kyvvu.eval": {
      "action": "allow",
      "risk_score": 0.0,
      "policies": []
    }
  },
  "timestamp": "2026-04-29T10:00:00+00:00"
}
```

### Field reference

| Field        | Type                 | Description                                                                                                                                                                                                                                                     |
| ------------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_id`   | string               | The registered agent's unique identifier.                                                                                                                                                                                                                       |
| `task_id`    | string               | Identifies this task (one end-to-end agent execution).                                                                                                                                                                                                          |
| `scope`      | `"task"` or `"step"` | Whether this is a task lifecycle event or an agent action.                                                                                                                                                                                                      |
| `step_type`  | string               | The atomic behaviour type (`step.model`, `step.resource`, `task.start`, etc.).                                                                                                                                                                                  |
| `verb`       | string or null       | HTTP-style verb (`GET`, `POST`, `PATCH`, `DELETE`) or null.                                                                                                                                                                                                     |
| `step_name`  | string               | A human-readable name for the step (function name, tool name).                                                                                                                                                                                                  |
| `input`      | dict or null         | The step's input data (function arguments, prompt, query).                                                                                                                                                                                                      |
| `output`     | dict or null         | The step's output data (function return value, response).                                                                                                                                                                                                       |
| `properties` | dict                 | Structured metadata — target system, auth scope, model info.                                                                                                                                                                                                    |
| `meta`       | dict or null         | Engine- and framework-reserved metadata. The engine stamps `kyvvu.eval` (the `action`, `risk_score`, and per-policy results from the pre-execution evaluation) on each recorded step; framework adapters add correlation fields (parent task ID, chain run ID). |
| `timestamp`  | ISO 8601             | When the step was recorded.                                                                                                                                                                                                                                     |

See [Atomic Behaviours](/core-concepts/behaviours) for the complete vocabulary of step types and valid combinations.

***

## Three kinds of "logging"

Kyvvu involves three distinct logging concepts. They are unrelated and should not be confused:

### 1. Application logging (Python logger)

Standard Python `logging` output from the Kyvvu engine and SDK. Controlled by `KV_LOG_LEVEL`. Set to `DEBUG` for detailed per-evaluation traces:

```
kyvvu_engine.engine DEBUG evaluate(): agent_id=agent-123 step_type=step.model → action=allow risk_score=0.00
```

This is diagnostic output for debugging. It goes to stderr.

### 2. Behavioral trace logging (engine batch flush)

The structured JSON Behaviors described above. By default, these are printed to **stdout** as JSON lines. On task completion (`end_task()`), the engine flushes all recorded steps.

To send traces to the Kyvvu platform API instead of stdout:

```bash
export KV_LOG_LOCATION=https://platform.kyvvu.com
export KV_LOG_FORMAT=kv
```

To disable trace output entirely:

```bash
export KV_LOG_LOCATION=none
```

### 3. Platform event logging (API events module)

Server-side audit trail in the Kyvvu platform. Records platform-level events (policy changes, user logins, agent registrations). Visible in the dashboard under Events. Not something you configure in the SDK.

***

## Reading the trace in the dashboard

When `KV_LOG_LOCATION` points to the platform API, behavioral traces appear in the dashboard:

1. Navigate to **Logs** in the sidebar.
2. Filter by agent, task ID, or time range.
3. Click a task to see the full ordered sequence of steps.
4. Each step shows its evaluation result (allow/warn/block), applicable policies, and the Behavior data.

The dashboard also validates the hash chain — a tamper-evident chain linking each log entry to the previous one. If any entry has been modified after ingestion, the chain validation will fail.

***

## Payload redaction

For GDPR-sensitive environments, you can redact input/output from the trace:

```bash
export KV_LOG_PAYLOADS=metadata_only
```

In this mode, each step's `input` and `output` are replaced with:

```json
{"redacted": true, "keys": ["prompt"], "length": 42}
```

Shape is preserved (you can see which fields were present and their approximate size), but content is stripped.

***

## Next steps

* [Atomic Behaviours](/core-concepts/behaviours) — the 12 behaviour types that form the vocabulary
* [Policies and Rules](/core-concepts/policies) — how policies evaluate against behaviours
* [Architecture](/core-concepts/architecture) — the three-package split and data flow


# Architecture

**What you'll learn:** The three-package split, why the engine is in-process, and how data flows from your agent to the platform.

***

## Three packages

Kyvvu is split into three packages with different licenses and deployment boundaries:

| Package            | License     | What it does                                                                                                                           |
| ------------------ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `kyvvu` (SDK)      | Apache 2.0  | Translates agent events into atomic Behaviors via templates. Manages agent registration, task lifecycle, and the `@kv.step` decorator. |
| `kyvvu-engine`     | BSL 1.1     | Evaluates policies against Behaviors. Stateful: tracks per-task history. Zero I/O — pure CPU.                                          |
| Kyvvu Platform API | Proprietary | Policy storage, incident management, behavioral trace ingestion, audit reporting, dashboard. Runs at `platform.kyvvu.com`.             |

`pip install kyvvu` installs both the SDK and the engine. The platform API is a hosted service.

## Why the engine is in-process

The engine runs inside your agent process, not as a remote service. This is a deliberate architectural choice.

**Performance.** Agent steps are frequent and small. A task can emit dozens of atomic Behaviors. If each one required a network round-trip to a policy service, you'd add milliseconds per step that compound into seconds per task. The engine evaluates policies in sub-millisecond time:

| Scenario                      | p99 latency |
| ----------------------------- | ----------- |
| 100 policies, 50-step history | 296 us      |
| 10 policies, empty history    | 34 us       |
| 0 policies (baseline)         | 3 us        |

At four orders of magnitude faster than a typical LLM call, the engine adds no perceptible latency.

**Coverage.** A gateway proxy only sees LLM calls. It misses tool invocations, resource reads, credential fetches, and decision gates between model calls — precisely where the governable behaviour lives. The in-process engine evaluates every step the agent takes.

**Resilience.** If the platform API is down, the engine continues evaluating with its cached policies. Agents don't stop working because a policy service is unreachable. For additional hardening:

* **Disk cache** (`KV_POLICY_CACHE_PATH`): policies are written to disk after each successful fetch and loaded on cold start if the API is unreachable — surviving process restarts during outages.
* **Fail-closed mode** (`KV_POLICY_FAIL_MODE=closed`): for high-risk agents, blocks all behaviors when no policies are available rather than failing open.
* **HMAC signing** (`KV_POLICY_HMAC_SECRET`): verifies policy integrity to prevent tampering by compromised proxies or MITM within the internal network.

See the [Configuration Reference](/deployment/configuration) for details on these settings.

For the full argument, see [The Hot Path Tax](https://kyvvu.com/blog/2026/04/29/hot-path-tax/).

## Policies on paths

The engine evaluates policies against the **full ordered history** of the current task, not just the current step. This is the "policies on paths" model, formalised in the paper [Runtime Governance for AI Agents: Policies on Paths](https://arxiv.org/abs/2603.16586).

Example: the same `step.exec` call is allowed or blocked depending on what happened earlier. If a `step.gate` (human approval) precedes it in the task history, it passes. If not, it's blocked. The decision is path-dependent.

This is what makes Kyvvu different from point-in-time content filters or LLM guardrails. A content filter checks one input at a time. Kyvvu checks the step in context — what the agent has already done, what resources it accessed, what approvals it received.

## Data flow

```
Agent code
  |
  |  @kv.step / LangChain handler
  v
SDK (template matching)
  |
  |  Behavior object
  v
Engine (PolicyEngine.evaluate)
  |
  |  Reads: cached policies + task history
  |  Returns: allow / warn / block
  v
Agent code (continues or catches KyvvuBlockedError)
  |
  |  On task completion: end_task()
  v
Engine (KyvvuRunner)
  |  Async: flush behavioral trace to log endpoint
  |  Async: fire incident webhook on violations
  v
Platform API
  |  Stores: logs, incidents, events
  v
Dashboard
```

### What happens synchronously (hot path)

* Template matching (SDK maps framework event to Behavior)
* Policy evaluation (engine runs rules against history)
* Step recording (engine appends to in-memory history)

All sub-millisecond, all in-process, all zero I/O.

### What happens asynchronously (background)

* Policy fetch (TTL-based, default every 300 seconds)
* Log flush (on `end_task()`, POST to log endpoint)
* Incident webhook (on `warn`/`block`, POST to incident endpoint)

Network failures in background operations are logged and swallowed. The engine never blocks on I/O during evaluation.

## One engine per agent

Each `KyvvuRunner` (and each `Kyvvu` SDK instance) owns one `PolicyEngine`. Engines are per-agent and are not shared across agents. This is by design — policies are scoped to specific agents, and task histories must not leak between agents.

For LangGraph, the `KyvvuLangChainHandler` treats the entire graph execution as a single task — all nodes (LLM calls, tool calls) are flattened into steps within that task. Each graph invocation is one task, not one task per node.

For truly separate agents (e.g., independent microservices or processes), each agent gets its own `Kyvvu` instance. Cross-agent coordination happens through pre-fetched aggregate counts in `EvalContext`, not through shared engine state.

***

## Next steps

* [Atomic Behaviours](/core-concepts/behaviours) — the 12 behaviour types the engine operates on
* [Policies and Rules](/core-concepts/policies) — how policies are structured and evaluated
* [Tasks and History](/core-concepts/tasks) — task lifecycle and path-dependent evaluation


# Atomic Behaviours

**What you'll learn:** The 12 behaviour types that form Kyvvu's canonical vocabulary, how `step_type + scope + verb` classifies every agent action, and where properties fit in.

***

## The vocabulary

Every action an agent takes is classified into one of 12 atomic behaviour types. Four describe the task lifecycle; eight describe the agent's actions within a task. Together with a **scope** (`task` or `step`) and an HTTP-style **verb** (`GET`, `POST`, `PATCH`, `DELETE`, or none), they form the vocabulary the engine operates on.

| Step type         | Scope  | Valid verbs           | What it represents                                                    |
| ----------------- | ------ | --------------------- | --------------------------------------------------------------------- |
| `task.start`      | `task` | --                    | A task begins.                                                        |
| `task.end`        | `task` | --                    | A task completes normally.                                            |
| `task.error`      | `task` | --                    | A task terminates with an error.                                      |
| `task.idle`       | `task` | --                    | The agent is idle within a task (heartbeat / keepalive).              |
| `step.resource`   | `step` | GET/POST/PATCH/DELETE | Read or mutate an external resource (DB, file, API).                  |
| `step.message`    | `step` | GET/POST              | Receive (GET) or send (POST) a message.                               |
| `step.self`       | `step` | GET/POST/PATCH/DELETE | Read/write the agent's own internal state (memory, scratchpad, plan). |
| `step.model`      | `step` | POST                  | Send a prompt to an LLM and receive a completion.                     |
| `step.credential` | `step` | GET                   | Retrieve a secret, token, or credential.                              |
| `step.exec`       | `step` | --                    | Execute code (run a script, call a function, shell out).              |
| `step.gate`       | `step` | --                    | Cross a gate — human approval, policy check, guardrail.               |
| `step.unknown`    | `step` | --                    | Uncategorisable behaviour (template fallback).                        |

These combinations are enforced by the `Behavior` model validator. Any `(step_type, scope, verb)` tuple outside the valid set raises on construction.

> **"Scope" means two different things.** The `scope` in the table above is the **behaviour scope** (`task` or `step`) — the granularity of a single atom. It is derived automatically from the `step_type` prefix (`task.*` → `task`, else `step`), so you never set it in a [behavioural template](/core-concepts/templates). The `scope:` you write in a **policy/manifest** is the **policy scope** (`agent_registration` or `step_execution`) — it controls *when* a policy is evaluated (see below), not the granularity of a behaviour. Same word, unrelated axes.

## Two evaluation points

Behaviours are evaluated at two points:

1. **Agent registration** — once, before the first task begins. Policies with `scope: agent_registration` run against the agent's metadata (purpose, tools, classification).
2. **Every step** — before each step executes. Policies with `scope: step_execution` run against the intended Behavior and the task's history.

`task.*` behaviours (start, end, error, idle) evaluate against `step_execution`-scoped policies, exactly like `step.*` behaviours. There is no separate "task\_execution" scope. Task lifecycle events are atomic steps that happen to sit at the boundaries of a task.

## Properties

Everything beyond the `(step_type, scope, verb)` tuple lives in `properties` — a nested dict that policies can inspect. Properties distinguish a `step.resource` reading `customer-data` from one reading `product-data`. The type is the same; the property is different.

Standard property groups:

| Group     | Purpose                                  | Example keys                                      |
| --------- | ---------------------------------------- | ------------------------------------------------- |
| `target`  | The thing being acted on                 | `domain`, `table`, `object_id`, `system`, `trust` |
| `auth`    | Authentication scope                     | `scope` (read/write/admin), `principal`           |
| `data`    | Payload classification                   | `classification` (pii, public), `fields`          |
| `model`   | LLM details (for `step.model`)           | `provider`, `name`, `parameters`                  |
| `exec`    | Execution details (for `step.exec`)      | `runtime`, `isolation`, `side_effect_class`       |
| `guard`   | Gate details (for `step.gate`)           | `gate_type` (human\_approval, policy\_check)      |
| `message` | Message details (for `step.message`)     | `channel`, `sender`, `recipient`                  |
| `usage`   | Usage metrics (for `step.model` outputs) | `prompt_tokens`, `completion_tokens`, `cost_usd`  |

Custom property groups are permitted. The engine passes them through unchanged, and rule functions read them via dot-path accessors (e.g. `target.table`, `data.classification`).

### Worked example

A `step.resource` GET reading customer data:

```python
from kyvvu_engine.schemas import Behavior, Scope, StepType, Verb

Behavior(
    agent_id="agent-123",
    task_id="task-abc",
    scope=Scope.step,
    step_type=StepType.step_resource,
    verb=Verb.GET,
    step_name="read_customer_record",
    input={"customer_id": "CUST-9981"},
    properties={
        "target": {
            "system": "salesforce",
            "table": "customer-data",
            "domain": "internal.crm.acme.com",
        },
        "auth": {"scope": "read", "principal": "agent-123"},
        "data": {"classification": "pii", "fields": ["name", "email", "phone"]},
    },
)
```

A policy using the `domain_allowlist` rule could check `target.domain`. A policy using `pii_in_request` could scan the input for sensitive patterns. The `history_contains` rule could check whether this behaviour has already occurred in the task.

***

## Mapping from frameworks

Templates bridge framework-specific events to the behavior vocabulary above. Each template is a YAML file with ordered rules that map event types (e.g. LangChain's `on_llm_start`, or a decorator's `step_type`) to `(step_type, scope, verb)` tuples with property extraction. See [Templates](/core-concepts/templates) for how this works, and the [Template Authoring Guide](/integrations/authoring) for how to write your own.

***

## Next steps

* [Policies and Rules](/core-concepts/policies) — how policies use behaviour types and properties to make decisions
* [Templates](/core-concepts/templates) — how the SDK maps framework events to this vocabulary
* [Built-in Rules Reference](/policy-authoring/rules-reference) — all 27 rules and which behaviour types they operate on


# Policies and Rules

**What you'll learn:** What a policy is, how rules work, the two scopes, and how severity maps to actions.

***

## What is a policy?

A **policy** is an instantiation of a **rule** with specific parameters, severity, scope, and optional scoping to an agent or risk classification.

```
policy = rule + params + scope + severity + (optional agent_id) + (optional risk_classification)
```

The same rule can back many policies. For example, `field_matches_regex` can be instantiated once to check for SSNs, once for credit card numbers, and once for email domains — three policies, one rule.

## What is a rule?

A **rule** (or rule function) is a pure Python function that returns `True` if the policy passes and `False` if it is violated:

```python
def rule(data: dict, params: dict, context: RuleContext) -> bool:
    """Return True if the policy passes; False if it is violated."""
```

Rules perform no I/O. They read the step's data, the policy's parameters, and the `RuleContext` (which provides access to task history and agent metadata). The engine ships with [27 built-in rules](/policy-authoring/rules-reference).

## Policy scopes

Policies have one of two scopes:

### `agent_registration`

Evaluated **once**, when an agent registers with the platform. These policies check the agent's metadata — purpose, declared tools, risk classification.

Example: "Agent must declare a substantive purpose" checks that the `purpose` field matches `^.{30,}$`.

### `step_execution`

Evaluated **before every step** the agent takes. These policies check the intended Behavior against the task's history and the agent's context.

Example: "Code execution requires a preceding gate" checks that a `step.gate` exists earlier in the task history before allowing `step.exec`.

## Severity and actions

Each policy has a severity level that maps to an action when the policy is violated:

| Severity   | Risk score | Action on violation | Engine behaviour                                                               |
| ---------- | ---------- | ------------------- | ------------------------------------------------------------------------------ |
| `low`      | 0.25       | `warn`              | `evaluate()` returns normally. Incident webhook fires if configured.           |
| `medium`   | 0.50       | `warn`              | `evaluate()` returns normally. Incident webhook fires if configured.           |
| `high`     | 0.75       | `warn`              | `evaluate()` returns normally. Incident webhook fires if configured.           |
| `critical` | 1.0        | `block`             | `evaluate()` raises `KyvvuBlockedError`. Incident webhook fires if configured. |

The mapping from severity to action uses a risk score. Each violated rule's boolean result is weighted by its severity level and aggregated. The default aggregator is `aggregate_max` (worst-case severity wins):

* Risk score `0.0` → `allow` (no violations)
* Risk score `(0.0, 1.0)` → `warn` (low, medium, or high severity violation)
* Risk score `1.0` → `block` (critical severity violation)

**Only `critical` severity blocks execution.** Lower severities (`low`, `medium`, `high`) produce warnings and incident reports but do not prevent the step from running. This is by design — most policies should warn during development and only block for truly critical safety boundaries.

## Policy scoping

Policies can be scoped to specific agents or risk classifications:

| Field                 | Effect                                                                           |
| --------------------- | -------------------------------------------------------------------------------- |
| `agent_id`            | Policy applies only to this agent. If null, applies to all agents.               |
| `risk_classification` | Policy applies only to agents with this classification. If null, applies to all. |
| `enabled`             | Toggle. Disabled policies are skipped during evaluation.                         |

## How policies are fetched

The engine fetches policies from the platform API and caches them in memory:

1. On first evaluation, the engine calls `GET /api/v1/policies?agent_key={agent_key}&enabled=true`.
2. Policies are cached for `KV_POLICY_TTL_SECONDS` (default: 300 seconds).
3. When the TTL expires, the engine re-fetches on the next `evaluate()` call.
4. If the API is unreachable, the engine continues with the previously cached policies.

Policy changes made in the dashboard or via the API take effect within the TTL window — no agent restart required.

***

## Next steps

* [Tasks and History](/core-concepts/tasks) — how task history enables path-dependent policies
* [Creating Policies](/policy-authoring/creating) — create your own policies
* [Compound Policies](/policy-authoring/compound) — combine rules with `all_of`, `any_of`, `not`


# Tasks and History

**What you'll learn:** What a task is, how history enables path-dependent policies, and how task lifecycle and memory management work.

***

## What is a task?

A **task** is one end-to-end agent execution, identified by a `task_id`. It represents everything an agent does from start to finish for a single request, conversation turn, or job.

The task's **history** is the ordered list of completed steps within it. History is what makes path-dependent policies possible — the engine doesn't just check the current step, it checks the current step *in the context of everything that came before*.

## Task lifecycle

```
start_task()
  |
  v
evaluate() -> execute -> record()    (repeat per step)
  |
  v
end_task()  or  error_task()
```

### `start_task()`

Begins a new task. Returns a `task_id`. Optionally accepts a caller-provided `task_id`.

The SDK emits a `task.start` Behavior that flows through the normal `evaluate()` path — registration-check and step policies both run.

### `evaluate()` / `record()` cycle

For each step within the task:

1. `evaluate(intended, context)` — checks the intended Behavior against policies and history. Returns `allow`, `warn`, or `block`.
2. The caller executes the step.
3. `record(completed_step)` — appends the step (with output) to the task's history. Assigns a monotonic step number.

### `end_task(task_id)`

Normal completion. The SDK emits a `task.end` Behavior through `evaluate()`, then calls `end_task()` on the engine. This:

* Evicts the task's history from memory
* Flushes buffered logs to the log endpoint (if configured)

No policies run in `end_task()` itself — it is cleanup. To enforce task-end invariants, use policies that match on the `task.end` behaviour type.

### `error_task(task_id)`

Abnormal termination. Same cleanup as `end_task()`, but emits a `task.error` Behavior first. History is evicted identically.

## Path-dependent evaluation

Because `evaluate()` reads the full task history, policies can express path-dependent constraints:

* "Code execution requires a preceding gate" — `step_requires_gate` checks whether `step.gate` exists anywhere in history before `step.exec`.
* "External content taint requires a fresh gate" — `step_directly_preceded_by` checks whether the *immediately previous* step was a `step.gate`.
* "No more than 50 resource calls" — `execution_max_steps` counts `step.resource` entries in history.
* "Forbidden sequence" — `sequence_forbidden` checks whether a specific ordered sequence has occurred.

The same step can be allowed or blocked depending on what happened earlier. This is the core of the "policies on paths" model.

## Memory management

Task history lives in memory, keyed by `task_id`. Three mechanisms prevent unbounded growth:

**Normal termination:** `end_task(task_id)` evicts history from memory and flushes buffered logs.

**Automatic background sweeper:** A daemon thread calls `sweep_stale_tasks()` every `KV_SWEEP_INTERVAL_SECONDS` (default: 300s / 5 minutes). Tasks older than `KV_TASK_MAX_AGE_SECONDS` (default: 3600s / 1 hour) are evicted automatically — no manual wiring needed.

**Pause-aware eviction:** When a task records a `task.idle` Behavior (e.g., waiting for human input), the sweep TTL clock resets. This prevents idle-but-legitimate tasks from being evicted prematurely. If the task remains idle beyond a full TTL window with no further activity, it is still evicted.

**Flush-before-evict:** When the sweeper evicts an abandoned task, it attempts a best-effort batch post of the buffered steps to the log endpoint (if `KV_SWEEP_FLUSH_ON_EVICT=true`, which is the default). This captures partial traces for debugging even when tasks crash.

| Config                      | Default | Purpose                                       |
| --------------------------- | ------- | --------------------------------------------- |
| `KV_TASK_MAX_AGE_SECONDS`   | `3600`  | Max age before a task is considered abandoned |
| `KV_SWEEP_INTERVAL_SECONDS` | `300`   | How often the sweeper runs                    |
| `KV_SWEEP_ENABLED`          | `true`  | Set to `false` to disable automatic sweeping  |
| `KV_SWEEP_FLUSH_ON_EVICT`   | `true`  | Post partial traces before discarding         |

A new `task_id` always starts with a fresh, empty history. Task histories are completely independent — there is no relationship between tasks.

## Concurrent tasks

In web servers and concurrent agents, multiple tasks may be active simultaneously. The SDK uses a `ContextVar` to track the active `task_id` per execution context (thread or asyncio task). The `@kv.step` decorator automatically reads the active task ID from this context.

For explicit control:

```python
task_id = kv.start_task()
# ... steps are associated with this task_id ...
kv.end_task()
```

***

## Next steps

* [Templates](/core-concepts/templates) — how the SDK maps framework events to Behaviors
* [Policies and Rules](/core-concepts/policies) — how policies are structured
* [Python Decorator](/integrations/decorator) — task lifecycle with `@kv.step`


# Templates

> **Note:** This page covers **behavior templates** — YAML rules that map framework events to Kyvvu Behaviors. For **policy manifests** (compliance policy sets assigned to agents), see [Manifests](/policy-authoring/templates).

**What you'll learn:** How YAML templates map framework events to Behaviors, how matching works, and how to customize templates.

***

## What templates do

Templates are the translation layer between your framework's events and Kyvvu's canonical vocabulary. When your agent calls an LLM, reads a database, or sends a message, the SDK needs to know which `(step_type, scope, verb)` tuple to use, and which properties to set. Templates define this mapping.

Without templates, every integration would need hardcoded mappings. With templates, the same SDK can support any framework — you just write a YAML file.

## Built-in templates

The template **engine** (loading, matching, interpolation) lives in `kyvvu_engine.templates`. The SDK ships the bundled template **YAML files** and a small `load(name)` helper that returns an engine `BehaviorTemplate`:

```python
from kyvvu.templates import load

decorator_template = load("decorator")
langchain_template = load("langchain")
```

The SDK bundles four per-integration templates:

* **`decorator`** — for the `@kv.step` decorator integration. Maps decorator arguments directly to Behavior fields.
* **`langchain`** — for the LangChain/LangGraph callback handler. Maps LangChain callback events (`on_llm_start`, `on_tool_start`, etc.) to Behaviors.
* **`langgraph`** — for LangGraph graph executions.
* **`crewai`** — for CrewAI agents.

Each integration (and kyvvu-claude) loads its own template by path. To load any template file directly, use the engine loader:

```python
from kyvvu_engine.templates import BehaviorTemplate, load_template

tpl = BehaviorTemplate.from_path("/path/to/template.yaml")  # or load_template(...)
```

## How matching works

Templates use **first-match-wins** rule evaluation. Each template contains an ordered list of rules. For each framework event, the SDK walks the rules top to bottom and uses the first match.

Each rule has:

* **Conditions** — predicates on the framework event (event type, function name, arguments).
* **Output** — the Behavior fields to set (`step_type`, `scope`, `verb`, `properties`).

If no rule matches, the template falls back to `step.unknown`.

## Deep-merge semantics

When the SDK constructs a Behavior, it deep-merges three sources (in order of priority):

1. **Caller overrides** — values passed explicitly via `@kv.step(properties={...})` or `kv.start_task(properties={...})`.
2. **Template output** — the matched rule's output.
3. **Framework defaults** — base values from the framework event itself.

**Caller overrides win at leaves.** If the template sets `properties.model.provider = "openai"` and the caller sets `properties.model.name = "gpt-4o"`, the result is `{model: {provider: "openai", name: "gpt-4o"}}`. But if the caller sets `properties.model.provider = "anthropic"`, the caller's value wins.

## Template properties vs. tool metadata

For LangChain and LangGraph, a tool can declare its own Kyvvu attributes under `metadata["kyvvu"]`, co-located with the tool definition. Those attributes **deep-merge over the template-matched properties**: the tool's value wins on any leaf conflict, siblings from the template are preserved, and a runtime warning is logged when a tool overrides a template-set property. In short, code wins over the org baseline. See [LangChain / LangGraph — Attaching Kyvvu attributes to tools](/integrations/langchain#attaching-kyvvu-attributes-to-tools-two-layers) for the full picture.

## Two tiers of developer effort

Templates support two levels of detail:

### Lazy (generic fallback)

Use the built-in templates without customization. Every step gets a correct `step_type` and `verb` based on the framework event. Properties are sparse — mostly framework-inferred defaults.

This is fine for getting started and for development-time tracing.

### Rich (named rules with properties)

Write a custom template with rules that match specific functions and set detailed properties — target system, data classification, auth scope, domain. This enables fine-grained policies that check properties like `target.table == "customer-data"` or `target.trust == "external"`.

This is what production deployments use.

## Loading custom templates

Two ways to use a custom template:

```python
from kyvvu_engine.templates import BehaviorTemplate

# Constructor kwarg
custom = BehaviorTemplate.from_path("/path/to/template.yaml")
kv = Kyvvu(api_key="...", agent_key="bot", template=custom)

# Environment variable (resolution order: template kwarg > KV_TEMPLATE_LOCATION > built-in)
# export KV_TEMPLATE_LOCATION=/path/to/template.yaml
kv = Kyvvu(api_key="...", agent_key="bot")
```

## Template YAML structure

A minimal template:

```yaml
name: my-agent-template
version: "1.0"

rules:
  - name: llm_call
    conditions:
      step_type: step.model
    output:
      properties:
        model:
          provider: openai
          name: gpt-4o

  - name: db_read
    conditions:
      step_name: read_customer
    output:
      step_type: step.resource
      scope: step
      verb: GET
      properties:
        target:
          system: postgres
          table: customer-data
          trust: internal
        data:
          classification: pii

  - name: fallback
    conditions: {}
    output:
      step_type: step.unknown
```

Rules are evaluated in order. The `fallback` rule with empty conditions matches everything and should be last.

***

## Next steps

* [Python Decorator](/integrations/decorator) — how `@kv.step` uses templates
* [LangChain / LangGraph](/integrations/langchain) — the LangChain callback handler and its template
* [Writing a New Integration](/integrations/custom) — how to author templates for other frameworks

## Further reading

* [Template Authoring Guide](/integrations/authoring) — step-by-step walkthrough for creating a custom behavioral template, including match semantics, property extraction, and a worked example.
* [Agent Pattern Templates](https://github.com/Kyvvu/platform/tree/main/kyvvu-sdk/kyvvu/templates/patterns/README.md) — reference snippets for common agent architectures (RAG, ReAct, human-in-the-loop, multi-agent) that show how to map these patterns to Kyvvu's behavior vocabulary.


# Claude Code

**What you'll learn:** How to add Kyvvu agent security to Claude Code sessions using the `kyvvu-claude` package.

***

## Overview

`kyvvu-claude` is a standalone Python package that integrates Claude Code into the Kyvvu ecosystem via Claude Code's native hook system. Every tool call (Bash, Read, Write, Edit, etc.) is evaluated against Kyvvu policies before execution, and all tool calls are logged as an immutable behavioral audit trail.

Unlike the SDK integrations (LangChain, CrewAI), kyvvu-claude does not wrap your code — it operates via Claude Code's hook system, so it works with any Claude Code session without code changes.

## Installation

```bash
pip install kyvvu-claude
```

## Quick start

```bash
# Interactive setup — connects to API, registers agent, installs hooks
kyvvu-claude init
```

You'll need:

* A Kyvvu API key (create one at [platform.kyvvu.com](https://platform.kyvvu.com))
* Python 3.10+ on macOS or Linux

After init, every Claude Code session is automatically governed.

## Architecture

```
Claude Code                          kyvvu-claude (hook process)
    |                                     |
    +-- SessionStart ──────────────> Fetch policies, start task
    +-- PreToolUse (per tool) ─────> Evaluate policies → allow / deny
    +-- PostToolUse (per tool) ────> Record step in audit trail
    +-- SessionEnd ────────────────> Flush trace to API, end task
```

Each hook invocation is a **separate short-lived process**. There is no long-running daemon. Session state (task ID, step history, policy cache) is persisted to `~/.kyvvu-claude/` between invocations.

### Policy evaluation flow

1. **SessionStart**: Fetch policies from the Kyvvu API (filtered by this agent's key). Cache to disk.
2. **PreToolUse**: Load policies from disk cache. Replay session history into the engine for path-dependent rules. Evaluate the intended tool call. Return `allow` or `deny`.
3. **PostToolUse**: Record the completed tool call in session history. Capture LLM calls from the transcript.
4. **SessionEnd**: Record `task.end`. Flush the full behavioral trace to the API.

## Enforcement

When a policy blocks a tool call:

1. kyvvu-claude returns `permissionDecision: "deny"` to Claude Code
2. Claude Code shows the block reason to the user/model
3. The blocked step is recorded in the audit trail with `output.status = "blocked"`
4. An incident is posted to the Kyvvu API
5. The model continues reasoning (it may try alternative approaches)

Tool blocks **deny the specific tool call** but **do not stop the session**. This is consistent with how Claude Code handles permission denials from users.

### Tainted-path enforcement

When Claude Code reads a secret file (`.env`, `.pem`, `.key`, etc.), kyvvu-claude classifies it with `data.classification: secret`. Tainted-path policies then block subsequent dangerous actions:

* `No exec after secret read` — blocks all Bash after a secret file read
* `No network after secret read` — blocks file reads outside the project after a secret file read

The taint is **permanent within the session**. Use `/clear` in Claude Code to start a new session and reset.

## Recommended manifest

Assign the **Claude Code Safety** manifest to your agent:

```
manifests/developer/claude-code-safety.yaml
```

9 policies covering credential exfiltration, destructive commands, scope containment, runaway prevention, and PII scanning. All policies are severity `critical` (= block).

## Configuration reference

All settings are in `~/.kyvvu-claude/config.json`, configured during `kyvvu-claude init`:

| Setting             | Default                      | Description                                                                          |
| ------------------- | ---------------------------- | ------------------------------------------------------------------------------------ |
| `api_url`           | `https://platform.kyvvu.com` | Kyvvu API URL                                                                        |
| `log_location`      | (same as api\_url)           | Where logs go: URL, file path, `stdout`, or `none`                                   |
| `log_format`        | `kv`                         | Log format: `kv`, `json`, or `otlp`                                                  |
| `incident_location` | (inherits trace sink)        | Where incidents go. Same vocabulary as `log_location`; empty inherits the trace sink |
| `incident_format`   | (inherits log\_format)       | Incident format: `kv`, `json`, or `otlp`. Empty inherits `log_format`                |
| `enforce`           | `false`                      | `true` = block violations, `false` = observe only (warn, don't block)                |
| `flush_threshold`   | `100`                        | Flush logs every N steps (0 = session end only)                                      |

### OTLP export

To send traces to an OpenTelemetry collector (Jaeger, Grafana Tempo, etc.):

```bash
kyvvu-claude init
# Log location: http://localhost:4318
# Log format: otlp
```

## CLI reference

| Command                        | Description                    |
| ------------------------------ | ------------------------------ |
| `kyvvu-claude init`            | Interactive setup              |
| `kyvvu-claude install-hooks`   | Install hooks into Claude Code |
| `kyvvu-claude uninstall-hooks` | Remove hooks                   |
| `kyvvu-claude status`          | Show configuration and status  |
| `kyvvu-claude policies`        | Display cached policies        |
| `kyvvu-claude refresh`         | Force policy refresh           |

## Compatibility

Works with all Claude Code surfaces:

* Claude Code CLI
* VS Code extension
* JetBrains plugin
* GitHub Actions (`anthropics/claude-code-action`)

**Not supported:** Claude Desktop (uses MCP, not hooks).

## Troubleshooting

### "Policies: 0 loaded" but policies are assigned

The policy cache may be stale. Run:

```bash
kyvvu-claude refresh
```

### Agent not appearing in dashboard

Clear the registration cache and re-initialize:

```bash
rm -f ~/.kyvvu/registrations/claude-code-*.json
rm -rf ~/.kyvvu-claude
kyvvu-claude init
```

### Hooks not firing

Check that hooks are installed:

```bash
grep "kyvvu-claude" ~/.claude/settings.json
```

If not present, run `kyvvu-claude install-hooks`.

### LLM calls missing from trace

LLM calls are captured from Claude Code's session transcript. In very short sessions (<10 seconds), the transcript may not be flushed to disk before the hook reads it. This is a known limitation.


# Python Decorator

**What you'll learn:** How to use `@kv.step` to instrument custom Python agents, including task lifecycle, async support, and all available options.

***

## Basic usage

Wrap each function with `@kv.step(step_type, verb)`. The decorator handles the evaluate-execute-record cycle automatically.

```python
from kyvvu import Kyvvu, StepType, Verb

kv = Kyvvu(api_url="https://platform.kyvvu.com", api_key="KvKey-...", agent_key="my-bot")
kv.register_agent(name="My Bot", purpose="Customer support triage and response")

@kv.step(StepType.step_model, Verb.POST)
def chat(prompt: str) -> str:
    return llm.complete(prompt)

@kv.step(StepType.step_resource, Verb.GET)
def read_ticket(ticket_id: str) -> dict:
    return db.get_ticket(ticket_id)
```

When `chat("Hello")` is called:

1. The decorator constructs a `Behavior` with `step_type=step.model`, `verb=POST`, `step_name="chat"`.
2. It calls `evaluate()` — the engine checks policies against the task history.
3. If allowed, the function executes.
4. The completed step (with output) is recorded into history via `record()`.
5. If blocked, `KyvvuBlockedError` is raised and the function does not execute.

## Task lifecycle

Use `StepType.task_start` and `StepType.task_end` to mark task boundaries:

```python
@kv.step(StepType.task_start)
def begin_task(self):
    return self._read_inbox()

@kv.step(StepType.task_end)
def finish(self):
    pass  # triggers log flush
```

Or use the programmatic API:

```python
task_id = kv.start_task()
try:
    result = my_agent_function()
except Exception as e:
    kv.error_task(error=e)
    raise
else:
    kv.end_task()
```

All three lifecycle methods (`start_task`, `end_task`, `error_task`) accept optional `context=`, `properties=`, and `meta=` kwargs for template matching and caller overrides.

## Decorator arguments

| Argument         | Type       | Default       | Description                                                                        |
| ---------------- | ---------- | ------------- | ---------------------------------------------------------------------------------- |
| `step_type`      | `StepType` | required      | The atomic behaviour type (`StepType.step_model`, `StepType.step_resource`, etc.). |
| `verb`           | `Verb`     | `None`        | HTTP-style verb (`Verb.GET`, `Verb.POST`, etc.).                                   |
| `step_name`      | `str`      | function name | Human-readable step name. Defaults to the decorated function's `__name__`.         |
| `properties`     | `dict`     | `{}`          | Structured metadata merged into the Behavior's properties.                         |
| `meta`           | `dict`     | `None`        | Framework-specific metadata (parent task ID, etc.).                                |
| `capture_input`  | `bool`     | `True`        | Whether to capture function arguments as the Behavior's `input`.                   |
| `capture_output` | `bool`     | `True`        | Whether to capture the return value as the Behavior's `output`.                    |

## Properties example

```python
@kv.step(StepType.step_model, Verb.POST,
         properties={"model": {"name": "gpt-4o", "provider": "openai"}})
def generate_reply(self, email):
    return llm.complete(email["body"])

@kv.step(StepType.step_resource, Verb.GET,
         properties={"target": {"system": "salesforce", "table": "customer-data"}})
def get_customer(self, customer_id):
    return sf.query(customer_id)
```

Properties are deep-merged with template output. See [Templates](/core-concepts/templates) for merge semantics.

## Async support

The decorator automatically detects `async def` functions:

```python
@kv.step(StepType.step_model, Verb.POST)
async def chat(prompt: str) -> str:
    return await openai_client.chat.completions.create(
        model="gpt-4o", messages=[{"role": "user", "content": prompt}]
    )
```

Policy evaluation and recording are synchronous (sub-millisecond, in-process). Only the decorated function call is awaited. Error handling, blocked-step propagation, and task lifecycle work identically to sync functions.

## Concurrent tasks in web servers

In web servers where multiple requests are handled concurrently, the SDK uses a `ContextVar` to track the active `task_id` per execution context. Each thread or asyncio task gets its own task.

```python
# Each request handler starts its own task:
task_id = kv.start_task()
# ... all @kv.step calls in this context use this task_id ...
kv.end_task()
```

## Handling blocks

When a policy blocks a step, the decorator raises `KyvvuBlockedError`:

```python
from kyvvu import KyvvuBlockedError

try:
    result = run_dangerous_action()
except KyvvuBlockedError as exc:
    print(f"Blocked: {exc}")
    print(f"Risk score: {exc.result.risk_score}")
    for p in exc.result.policies:
        if p.violated:
            print(f"  - {p.name} ({p.severity})")
    # Decide: retry with different approach, ask for approval, abort
```

## How the decorator uses templates

The decorator passes its arguments through the template system before constructing a Behavior. The template can:

* Map `step_name` to additional properties (e.g. "if step\_name starts with `db_`, set `target.system = postgres`").
* Override `step_type` based on naming conventions.
* Set default properties for all steps of a given type.

To use a custom template:

```python
from kyvvu_engine.templates import BehaviorTemplate

template = BehaviorTemplate.from_path("my_template.yaml")
kv = Kyvvu(api_key="...", agent_key="bot", template=template)
```

***

## Next steps

* [LangChain / LangGraph](/integrations/langchain) — callback-based integration for LangChain agents
* [Templates](/core-concepts/templates) — how template matching and deep-merge work
* [Tasks and History](/core-concepts/tasks) — task lifecycle and path-dependent evaluation


# LangChain / LangGraph

**What you'll learn:** How to instrument LangChain and LangGraph agents using the `KyvvuLangChainHandler` callback handler.

***

## Setup

The same handler works for both LangChain and LangGraph — LangGraph uses `langchain_core` callbacks under the hood.

```python
from kyvvu import Kyvvu
from kyvvu.schemas import Environment, RiskClassification
from kyvvu.integrations.langchain import KyvvuLangChainHandler

kv = Kyvvu(
    api_key="KvKey-...",
    api_url="https://platform.kyvvu.com",
    agent_key="finance-agent",
    environment=Environment.DEVELOPMENT,
    risk_classification=RiskClassification.LIMITED,
)
kv.register_agent(
    name="Finance Agent",
    purpose="Stock ticker lookup and portfolio analysis for internal use",
    metadata={"framework": "langchain", "tools": ["search"]},
)

handler = KyvvuLangChainHandler(kv)
result = agent.invoke(query, config={"callbacks": [handler]})
```

The handler is a pure adapter. It doesn't manage identity or registration — the same `Kyvvu()` + `register_agent()` pattern as the decorator integration.

## Callback event mapping

The handler translates LangChain callback events to Kyvvu Behaviors. The built-in `langchain` template defines these mappings (rules are first-match-wins; order matters):

| LangChain callback                | Kyvvu step\_type | Verb   | Notes                                                                                                                          |
| --------------------------------- | ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `on_chain_start` (top-level)      | `task.start`     | —      | Opens a new task; also emits a `step.message` GET for the user input                                                           |
| `on_chain_end` (top-level)        | `task.end`       | —      | Closes the task; emits a `step.message` POST for the response                                                                  |
| `on_chain_error` (top-level)      | `task.error`     | —      | Chain raised an exception — task failed                                                                                        |
| `on_agent_finish`                 | `task.end`       | —      | Agent produced its final answer — closes the task                                                                              |
| `on_message_in`                   | `step.message`   | `GET`  | User input received at chain start                                                                                             |
| `on_message_out`                  | `step.message`   | `POST` | Response sent to the user at chain end                                                                                         |
| `on_llm_start`                    | `step.model`     | `POST` | Completion-style LLM call; model name in `properties.model.name`                                                               |
| `on_chat_model_start`             | `step.model`     | `POST` | Chat-style LLM call; model name in `properties.model.name`                                                                     |
| `on_retriever_start`              | `step.resource`  | `GET`  | Read-only retrieval; `properties.target.resource_type = retriever`                                                             |
| `on_tool_start` (read-like name)  | `step.resource`  | `GET`  | Name matches `search`/`fetch`/`read`/`get`/`list`/`query`/`retrieve`/`lookup`/`find`/`scan`                                    |
| `on_tool_start` (write-like name) | `step.resource`  | `POST` | Name matches `write`/`send`/`post`/`create`/`update`/`delete`/`remove`/`execute`/`run`/`call`/`patch`/`push`/`submit`/`upload` |
| `on_tool_start` (unclassified)    | `step.resource`  | `GET`  | Default when no name pattern matches                                                                                           |
| `on_custom_event`                 | `step.unknown`   | —      | Application-defined `adispatch_custom_event`                                                                                   |

Tool verbs are derived from the tool's **name pattern**: read-like names map to `GET`, write-like names to `POST`, and anything else falls back to `GET`. Each event goes through the template system; a custom template can override any of these mappings.

## Nested chain resolution

LangChain/LangGraph agents often produce deeply nested chain callbacks. The handler resolves nested chains to keep the behavioral trace readable:

* Top-level chain events produce Behaviors.
* Deeply nested internal chains are collapsed unless they represent distinct logical steps.
* Tool calls within chains are always surfaced.

## Block propagation

When a policy blocks a step during a LangChain callback:

1. The handler receives the `KyvvuBlockedError`.
2. With `raise_error=True` (default), the error propagates up through the LangChain callback chain, halting execution.
3. The agent code can catch `KyvvuBlockedError` at the top level.

```python
from kyvvu import KyvvuBlockedError

handler = KyvvuLangChainHandler(kv)
try:
    result = agent.invoke(query, config={"callbacks": [handler]})
except KyvvuBlockedError as exc:
    print(f"Policy blocked: {exc}")
```

## Task lifecycle

The handler automatically manages task lifecycle:

* A task starts on the first callback event.
* The task ends when the top-level chain completes.
* If the chain raises an exception, `error_task()` is called.

## Attaching Kyvvu attributes to tools: two layers

Out of the box, the built-in `langchain` template gives every step a correct `step_type` and `verb`. To drive richer policies you often want extra properties on a step — a data classification, a target system, a trust level. There are two first-class ways to attach them, and they compose.

### Org-wide behavior template (base layer)

A **behavior template** is a shared YAML file (`.btmpl` / `.yaml`) that maps tools by name-pattern to Kyvvu properties. Point the handler at it with the `KV_TEMPLATE_LOCATION` environment variable or the `template=` constructor kwarg. Because it lives in one file, it is the natural home for **fleet-wide policy**: one team owns it, every agent that loads it inherits the same baseline, and you can classify tools you don't own without touching their code.

```python
from kyvvu_engine.templates import BehaviorTemplate
from kyvvu.integrations.langchain import KyvvuLangChainHandler

template = BehaviorTemplate.from_path("org_langchain_template.yaml")
kv = Kyvvu(api_key="...", agent_key="finance-agent", template=template)
handler = KyvvuLangChainHandler(kv)
```

The template rule below classifies any tool named `get_invoice` as financial data. It matches on the tool name and sets the same property a tool could set about itself:

```yaml
rules:
  - id: invoice_tool
    description: Invoice lookups handle financial data
    match:
      event: "on_tool_start"
      name_pattern: "^get_invoice$"
    behavior:
      step_type: "step.resource"
      verb: "GET"
      step_name: "{{ name }}"
      properties:
        data:
          classification: financial
```

Rules are first-match-wins and fall through to the built-in `langchain` template for any event the org template doesn't match.

### Per-tool `metadata["kyvvu"]` (local override)

A tool can also declare its own Kyvvu attributes, co-located with the tool definition, under the reserved `metadata["kyvvu"]` key. The value is shaped exactly like a step's `properties`:

```python
from langchain_core.tools import Tool

get_invoice = Tool(
    name="get_invoice",
    func=fetch_invoice,
    description="Fetch an invoice by ID",
    metadata={"kyvvu": {"data": {"classification": "financial"}}},
)
```

This produces the same `data.classification: financial` property as the org template rule above — but it travels with the tool, which is ideal for tools **you own** or for local nuance the fleet template shouldn't have to know about. The handler forwards `metadata["kyvvu"]` for tools, retrievers, and models (`on_tool_start`, `on_retriever_start`, `on_llm_start` / `on_chat_model_start`).

### Precedence

When both layers set a property, **tool metadata deep-merges over the template-matched properties**: the tool's value wins on any leaf conflict, and sibling keys from the template are preserved. So in the example above, if the org template classified `get_invoice` as `internal` and the tool declared `financial`, the emitted step carries `financial`. A deduped runtime warning is logged whenever a tool overrides a template-set property, so silent divergence is visible in your logs.

### When to use which

* **Org template** — fleet-wide policy, consistent classification across many agents, and tools you don't own (third-party or vendor tools). One file, one owner.
* **Per-tool metadata** — tools you own, or a local nuance that belongs next to the tool definition. Wins on conflict, so it's also the escape hatch when a specific tool needs to diverge from the baseline.

### Framework tags

Framework `tags` on a run are forwarded into `properties.tags`, minus internal LangChain/LangGraph bookkeeping tags (those prefixed `seq:`, `graph:`, or `langsmith:`). With no metadata and no tags, step properties are unchanged from the template match.

## LangGraph-specific notes

LangGraph agents use the same handler. Pass via `config`:

```python
from langgraph.graph import StateGraph

graph = StateGraph(...)
# ... build graph ...
app = graph.compile()

handler = KyvvuLangChainHandler(kv)
result = app.invoke(input_data, config={"callbacks": [handler]})
```

The behavioral trace stays flat — there are no node-transition pseudo-steps. Tool calls within nodes appear as `step.resource` events, and (with `KyvvuLangGraphHandler`) each step records its originating node under `properties.langgraph.node_name`.

### Dedicated LangGraph handler

For richer graph-aware tracing, use `KyvvuLangGraphHandler` (`kyvvu.integrations.langgraph`), a subclass of `KyvvuLangChainHandler` that adds two things:

* **Node metadata** — injects `properties.langgraph.node_name` from the callback metadata, so the trace records which graph node each step ran in.
* **Interrupt mapping** — maps LangGraph's `GraphInterrupt` (raised by `interrupt()`) to a `step.gate` behavior, so human-in-the-loop breakpoints are governable by gate policies.

```python
from kyvvu.integrations.langgraph import KyvvuLangGraphHandler

handler = KyvvuLangGraphHandler(kv)
result = app.invoke(input_data, config={"callbacks": [handler]})
```

The shared `KyvvuLangChainHandler` still works for LangGraph; the dedicated handler is opt-in when you want node metadata or HITL gate mapping.

### Parallel tool calls within a step

When the LLM emits multiple `tool_use` blocks and the runtime dispatches them concurrently (e.g., `asyncio.gather` in LangGraph), Kyvvu sees N sibling tool calls under the same parent `run_id`. The behavioral trace timestamps them in completion order, not invocation order.

**Path policies operate across steps, not within them.** A policy like "must call X before Y" is well-defined when X and Y are in different steps (X in step 1, Y in step 2). It is ill-defined when X and Y are dispatched concurrently within the same step — their ordering is non-deterministic. Policy authors should specify ordering constraints across steps, not between parallel calls within one.

**Kyvvu evaluates the path; it does not serialise the dispatcher.** If a policy requires sequential execution, the developer must enforce it in the runtime — sequential awaits, or a single-tool-per-step constraint in the agent prompt. Kyvvu records what happened; it does not control dispatch order.

***

## Integration status

| Framework                 | Status    | Notes                                                                                                                    |
| ------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------ |
| LangChain                 | Available | Full support via `KyvvuLangChainHandler`.                                                                                |
| LangGraph                 | Available | Shared `KyvvuLangChainHandler`, or `KyvvuLangGraphHandler` for node metadata and `GraphInterrupt` → `step.gate` mapping. |
| CrewAI                    | Available | Native support via `KyvvuCrewAIListener` (`kyvvu.integrations.crewai`), wired to the CrewAI event bus.                   |
| Microsoft AutoGen         | Planned   | Native adapter in development.                                                                                           |
| Microsoft Semantic Kernel | Planned   | Native adapter in development.                                                                                           |

Interested in a framework not listed here? Contact us at <support@kyvvu.com>.

***

## Next steps

* [Python Decorator](/integrations/decorator) — alternative integration for custom Python agents
* [Writing a New Integration](/integrations/custom) — build a handler for another framework
* [Templates](/core-concepts/templates) — customize the event-to-Behavior mapping


# Writing a Custom Template

**What you'll learn:** How to customize the way framework events map to Kyvvu atomic behaviors — without writing any Python code.

***

## When do you need a custom template?

The built-in templates (`langchain`, `decorator`) work out of the box for most agents. You need a custom template when you want to:

* **Add metadata to specific tools** — e.g., mark `fetch_customer_pii` as `contains_pii: true` so policies can act on PII-handling steps
* **Change verb classification** — e.g., your `archive_record` tool is a destructive DELETE, not the default GET
* **Add domain-specific patterns** — e.g., all tools starting with `db_` should be tagged as `resource_type: "database"`
* **Add new event types** — e.g., map a custom callback event to `step.gate`

You do **not** need a custom template to add policies — policies are separate from templates. Templates control *what behaviors are emitted*; policies control *what happens when those behaviors are evaluated*.

## How templates work

A template is a YAML file with an ordered list of rules. Each rule has:

* **`match`** — conditions that match against the event context (event name, tool name, etc.)
* **`behavior`** — the Kyvvu atomic behavior to emit when the rule matches

Rules are **first-match-wins** — the engine tries each rule in order and uses the first one that matches. Order matters.

## Starting from the built-in template

The easiest way to create a custom template is to copy and modify the built-in one. The LangChain template is at:

```
kyvvu-sdk/kyvvu/templates/langchain.template.yaml
```

Copy it to your project:

```bash
cp $(python -c "import kyvvu.templates; print(kyvvu.templates.__path__[0])")/langchain.template.yaml ./my-template.yaml
```

## Loading your custom template

Two ways:

### Environment variable (recommended for deployment)

```bash
export KV_TEMPLATE_LOCATION=./my-template.yaml
```

The handler picks this up automatically — no code changes needed.

### Constructor parameter (explicit)

```python
from kyvvu_engine.templates import BehaviorTemplate
from kyvvu.integrations.langchain import KyvvuLangChainHandler

template = BehaviorTemplate.from_path("./my-template.yaml")
handler = KyvvuLangChainHandler(kv, template=template)
```

## Template YAML format

```yaml
name: my-company-langchain
version: "1.0"
description: |
  Custom template for Acme Corp's LangChain agents.

rules:
  - id: rule_identifier
    description: Human-readable explanation
    match:
      event: "on_tool_start"          # exact match on event name
      name_pattern: "^fetch_.*"       # regex match on tool/step name
    behavior:
      step_type: "step.resource"      # atomic behavior type
      verb: "GET"                     # HTTP-style verb
      step_name: "{{ name }}"         # Jinja2 template — inserts the tool name
      properties:                     # custom metadata for policy evaluation
        target:
          resource_type: "database"
          contains_pii: true
```

### Match fields

| Field          | Type   | Description                                                                    |
| -------------- | ------ | ------------------------------------------------------------------------------ |
| `event`        | string | Exact match on the callback event name (e.g., `on_tool_start`, `on_llm_start`) |
| `name_pattern` | regex  | Regex match on the tool/step name. Tested against the `name` context key.      |

Both fields are optional. If both are present, both must match. If neither is present, the rule matches everything (catch-all).

### Behavior fields

| Field        | Type   | Description                                                                                                                   |
| ------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `step_type`  | string | Atomic behavior type: `step.model`, `step.resource`, `step.exec`, `step.gate`, `step.message`, `task.start`, `task.end`, etc. |
| `verb`       | string | `GET`, `POST`, `PUT`, `DELETE`, or omit for types that don't use verbs (gates, tasks)                                         |
| `step_name`  | string | Name for the step. Supports Jinja2 templates: `{{ name }}`, `{{ serialized.name }}`                                           |
| `properties` | dict   | Arbitrary metadata attached to the behavior. Policies can inspect these via rule parameters.                                  |

### Available context keys (LangChain)

These are available in `match` conditions and `{{ }}` template expressions:

| Key          | Source                               | Example                       |
| ------------ | ------------------------------------ | ----------------------------- |
| `event`      | Callback method name                 | `"on_tool_start"`             |
| `name`       | Tool/model/chain name                | `"fetch_customer"`            |
| `serialized` | LangChain serialized dict            | `{"name": "ChatOpenAI", ...}` |
| `inputs`     | Chain inputs (on\_chain\_start only) | `{"messages": [...]}`         |
| `outputs`    | Chain outputs (on\_chain\_end only)  | `{"output": "..."}`           |
| `task_id`    | Active task ID                       | `"abc-123"`                   |

## Common customizations

### Mark specific tools as PII-handling

Add a rule **before** the default `tool_read` catch-all:

```yaml
rules:
  # ... task lifecycle rules ...

  # Custom: PII tools get special properties
  - id: pii_tool
    description: Tools that handle personally identifiable information
    match:
      event: "on_tool_start"
      name_pattern: "^(fetch_customer|lookup_user|get_profile|search_patients).*"
    behavior:
      step_type: "step.resource"
      verb: "GET"
      step_name: "{{ name }}"
      properties:
        target:
          resource_type: "tool"
          contains_pii: true

  # ... rest of the default rules ...
```

Now you can write a policy that blocks PII tools without a preceding gate.

### Classify a tool as destructive (DELETE)

```yaml
  - id: archive_tool
    description: archive_record is a destructive operation
    match:
      event: "on_tool_start"
      name_pattern: "^archive_record$"
    behavior:
      step_type: "step.resource"
      verb: "DELETE"
      step_name: "{{ name }}"
      properties:
        target:
          resource_type: "tool"
          destructive: true
```

### Tag all database tools

```yaml
  - id: db_tools
    description: All db_ prefixed tools are database operations
    match:
      event: "on_tool_start"
      name_pattern: "^db_.*"
    behavior:
      step_type: "step.resource"
      verb: "{{ name | regex_replace('^db_write.*', 'POST') | regex_replace('^db_read.*', 'GET') | default('GET') }}"
      step_name: "{{ name }}"
      properties:
        target:
          resource_type: "database"
```

## Rule ordering tips

1. **Specific rules first, catch-all last.** The first matching rule wins.
2. **Keep task lifecycle rules at the top.** Don't accidentally match `on_chain_start` with a tool rule.
3. **Keep the default `tool_default` catch-all as the last tool rule.** Custom tool rules go between `tool_write` and `tool_default`.
4. **Test with `KV_LOG_LEVEL=DEBUG`.** The engine logs which rule matched for each event.

## Debugging

Set `KV_LOG_LEVEL=DEBUG` to see template matching in action:

```
kyvvu: matched context {'event': 'on_tool_start', 'name': 'fetch_customer'} -> {'step_type': 'step.resource', 'verb': 'GET', 'step_name': 'fetch_customer', 'properties': {'target': {'contains_pii': true}}}
```

If you see `kyvvu: unmatched event context`, your template is missing a rule for that event type.

***

## Next steps

* [LangChain / LangGraph Integration](/integrations/langchain) — the built-in handler these templates work with
* [Atomic Behaviours](/core-concepts/behaviours) — the full vocabulary of behavior types
* [Writing a New Integration](/integrations/custom) — for non-LangChain frameworks that need a Python adapter


# Template Authoring Guide

**What you'll learn:** How to create a custom behavioral template that maps your framework's events to Kyvvu's 12 atomic behavior types.

***

## When you need a custom template

The SDK ships four built-in templates:

* **`decorator`** -- for Python agents using `@kv.step` decorators.
* **`langchain`** -- for LangChain agents using the callback handler.
* **`langgraph`** -- for LangGraph graph executions.
* **`crewai`** -- for CrewAI agents.

You need a custom template when you're integrating a framework that none of the built-ins cover and isn't using `@kv.step` decorators. Examples: a custom HTTP-based agent, a Microsoft AutoGen agent, or any framework that emits its own event types. The template **engine** (loading, matching, interpolation) lives in `kyvvu_engine.templates`; the YAML files live with their integration.

## Template YAML anatomy

A template is a YAML file with three top-level keys and an ordered list of rules. Here's an annotated excerpt from the built-in `decorator` template:

```yaml
name: decorator                    # Template identifier
version: "1.0"                     # Version string (for your own tracking)
description: |                     # Optional human-readable description
  Maps Python decorator annotations to v0.05 Kyvvu atomic behaviors.

rules:                             # Ordered list of matching rules

  - id: model_call                 # Unique rule identifier
    description: |                 # What this rule matches (for documentation)
      AI model inference: LLM completion, chat, embedding, vision, etc.
    match:                         # Conditions -- all must be true for this rule to fire
      step_type: "step.model"
    behavior:                      # Output fields when the rule matches
      step_type: "step.model"
      verb: "POST"
      step_name: "{{ function_name }}"
      properties:
        model:
          name: "{{ kwargs.model | default('unknown') }}"
          provider: "{{ kwargs.provider | default('unknown') }}"

  - id: unknown                    # Catch-all fallback rule (should be last)
    match:
      step_type: "step.unknown"
    behavior:
      step_type: "step.unknown"
      step_name: "{{ function_name }}"
```

Each rule has:

| Key           | Required    | Purpose                                                           |
| ------------- | ----------- | ----------------------------------------------------------------- |
| `id`          | Recommended | Unique identifier for the rule (for debugging and documentation). |
| `description` | No          | Human-readable description.                                       |
| `match`       | Yes         | Conditions dict. All conditions must pass (implicit AND).         |
| `behavior`    | Yes         | Output dict with `step_type`, `verb`, `step_name`, `properties`.  |

> **Note:** Do not set `scope` in a template. The behavior's scope is derived automatically from `step_type` at emit time (`task.*` → `task`, everything else → `step`) by the decorator and every callback adapter, so a `scope:` key in the YAML is ignored.

## The context dict

When your integration fires, it builds a **context dict** and passes it to `template.match(context)`. The template's rules match against this context.

The context keys depend on your integration. Here are the two built-in examples:

**Decorator context keys:**

```python
{
    "step_type": "step.resource",       # From @kv.step(step_type=...)
    "verb": "GET",                       # From @kv.step(verb=...)
    "function_name": "fetch_customer",   # The decorated function's __name__
    "kwargs": {"url": "https://...", "customer_id": "C-123"},  # Call arguments
}
```

**LangChain context keys:**

```python
{
    "event": "on_llm_start",            # LangChain callback name
    "name": "ChatOpenAI",               # Run name or model name
    "serialized": {"name": "gpt-4o"},   # LangChain serialized object
    "inputs": {...},                     # Chain inputs (on_chain_start)
}
```

**Designing context keys for a new framework:**

1. Identify what events your framework emits (e.g. `http_request`, `llm_call`, `tool_invoke`).
2. Choose a primary discriminator key (e.g. `event` or `event_type`).
3. Include any metadata your rules need to distinguish sub-types (e.g. HTTP method, model name, tool name).
4. Include data you want to extract into properties (e.g. URL, model parameters).

## Match semantics

Rules are evaluated **top to bottom** in list order. The first rule whose `match` block fully matches the context wins. Evaluation stops immediately -- no further rules are checked.

### Exact string matching

Each key in `match` is compared to the same key in the context dict. All keys must match (implicit AND):

```yaml
match:
  event: "on_tool_start"     # context["event"] == "on_tool_start"
  verb: "GET"                # AND context["verb"] == "GET"
```

### Nested keys (dot-path)

Use dot notation to match nested dict values:

```yaml
match:
  serialized.name: "gpt-4o"  # context["serialized"]["name"] == "gpt-4o"
```

### Regex patterns

Two special condition keys support regex matching:

* `name_pattern` -- regex match against `context["name"]` (case-insensitive)
* `function_name_pattern` -- regex match against `context["function_name"]` (case-insensitive)

```yaml
match:
  event: "on_tool_start"
  name_pattern: "^(search|fetch|read|get).*"   # Tool name starts with a read verb
```

### Why rule order matters

Because of first-match-wins, **specific rules must come before general ones**. For example, in the LangChain template, tool rules are ordered: read-pattern tools, then write-pattern tools, then the catch-all tool default. If the catch-all were first, it would match every tool and the specific patterns would never fire.

### The fallback/default rule

A rule with an empty `match` block matches everything:

```yaml
- id: fallback
  match: {}
  behavior:
    step_type: "step.unknown"
    step_name: "unrecognized"
```

Every template should end with a fallback rule so that unrecognized events produce a `step.unknown` behavior instead of returning `None` (no match).

## Property extraction

String values in the `behavior` block support `{{ }}` interpolation from the context dict.

### Simple lookup

```yaml
step_name: "{{ function_name }}"
```

Resolves `context["function_name"]`. If the key is missing, produces an empty string.

### Default values

```yaml
name: "{{ kwargs.model | default('unknown') }}"
```

If `context["kwargs"]["model"]` is missing or `None`, produces `"unknown"`.

### Nested paths

```yaml
host: "{{ kwargs.url | default('') }}"
```

Traverses `context["kwargs"]["url"]` using dot-path notation.

### Mapping to Kyvvu's property schema

Properties are grouped by purpose. The standard groups (from the [Behaviours reference](/core-concepts/behaviours)) are:

| Group     | Used with            | Example keys                                                  |
| --------- | -------------------- | ------------------------------------------------------------- |
| `target`  | `step.resource`      | `host`, `resource_type`, `system`, `table`, `domain`, `trust` |
| `model`   | `step.model`         | `name`, `provider`, `temperature`, `parameters`               |
| `guard`   | `step.gate`          | `check_type`, `result`                                        |
| `message` | `step.message`       | `channel`, `recipient`, `sender`                              |
| `auth`    | `step.credential`    | `credential_name`, `provider`, `scope`                        |
| `data`    | any                  | `classification`, `fields`                                    |
| `exec`    | `step.exec`          | `runtime`, `isolation`, `side_effect_class`                   |
| `usage`   | `step.model` outputs | `prompt_tokens`, `completion_tokens`, `cost_usd`              |

Example property extraction in a rule:

```yaml
behavior:
  step_type: "step.resource"
  verb: "GET"
  step_name: "{{ function_name }}"
  properties:
    target:
      host: "{{ kwargs.url | default('') }}"
      resource_type: "{{ kwargs.resource_type | default('unknown') }}"
    data:
      classification: "{{ kwargs.data_class | default('') }}"
```

## Testing your template

Use `BehaviorTemplate.from_path()` to load your template and `match()` to verify mappings:

```python
from kyvvu_engine.templates import BehaviorTemplate

t = BehaviorTemplate.from_path("my_template.yaml")
result = t.match({"event": "http_request", "method": "GET", "url": "https://api.example.com"})
print(result)
# {'step_type': 'step.resource', 'verb': 'GET', ...}  # scope is derived later, not in the matched dict
```

Test each event type your framework emits. Test edge cases: missing fields, unknown events, events with `None` values. Verify the fallback rule catches unrecognized events.

## Worked example: HTTP agent template

Suppose you have an agent that emits these events:

| Event                | Data              |
| -------------------- | ----------------- |
| `http_request`       | `method`, `url`   |
| `llm_call`           | `model`, `prompt` |
| `approval_requested` | `approver`        |
| `task_started`       | --                |
| `task_completed`     | --                |

### Step 1: Task lifecycle rules

```yaml
name: http-agent
version: "1.0"
description: |
  Maps HTTP agent events to Kyvvu atomic behaviors.

rules:
  - id: task_start
    description: Agent begins a new task
    match:
      event: "task_started"
    behavior:
      step_type: "task.start"
      step_name: "task_started"

  - id: task_end
    description: Agent completes a task
    match:
      event: "task_completed"
    behavior:
      step_type: "task.end"
      step_name: "task_completed"
```

### Step 2: LLM call rule

```yaml
  - id: llm_call
    description: Agent calls an LLM
    match:
      event: "llm_call"
    behavior:
      step_type: "step.model"
      verb: "POST"
      step_name: "llm_call"
      properties:
        model:
          name: "{{ model | default('unknown') }}"
```

### Step 3: HTTP request rule

```yaml
  - id: http_request
    description: Agent makes an HTTP request
    match:
      event: "http_request"
    behavior:
      step_type: "step.resource"
      verb: "{{ method | default('GET') }}"
      step_name: "http_request"
      properties:
        target:
          host: "{{ url | default('') }}"
```

### Step 4: Approval gate

```yaml
  - id: approval
    description: Agent requests human approval
    match:
      event: "approval_requested"
    behavior:
      step_type: "step.gate"
      step_name: "approval_requested"
      properties:
        guard:
          check_type: "human_approval"
          result: "{{ result | default('pending') }}"
```

### Step 5: Fallback rule

```yaml
  - id: fallback
    description: Catch-all for unrecognized events
    match: {}
    behavior:
      step_type: "step.unknown"
      step_name: "{{ event | default('unknown') }}"
```

### Step 6: Test it

```python
from kyvvu_engine.templates import BehaviorTemplate

t = BehaviorTemplate.from_path("http_agent.template.yaml")

# Task lifecycle
r = t.match({"event": "task_started"})
assert r["step_type"] == "task.start"

# LLM call
r = t.match({"event": "llm_call", "model": "gpt-4o", "prompt": "Hello"})
assert r["step_type"] == "step.model"
assert r["properties"]["model"]["name"] == "gpt-4o"

# HTTP request
r = t.match({"event": "http_request", "method": "GET", "url": "https://api.example.com"})
assert r["step_type"] == "step.resource"
assert r["verb"] == "GET"
assert r["properties"]["target"]["host"] == "https://api.example.com"

# Approval gate
r = t.match({"event": "approval_requested", "approver": "admin@co.com", "result": "pass"})
assert r["step_type"] == "step.gate"
assert r["properties"]["guard"]["check_type"] == "human_approval"

# Unknown event falls to default
r = t.match({"event": "something_unexpected"})
assert r["step_type"] == "step.unknown"
```

***

## Next steps

* [Templates overview](/core-concepts/templates) -- deep-merge semantics and built-in template details
* [Atomic Behaviours](/core-concepts/behaviours) -- the 12 behavior types and property groups
* [Writing a New Integration](/integrations/custom) -- the `FrameworkAdapter` base class for hooking into frameworks
* [Pattern templates](https://github.com/Kyvvu/platform/blob/main/kyvvu-sdk/kyvvu/templates/patterns/README.md) -- reference snippets for common agent architectures (RAG, ReAct, human-in-the-loop, multi-agent)


# Writing a New Integration

**What you'll learn:** How to integrate Kyvvu with a framework that isn't covered by the built-in decorator or LangChain handler, by authoring a YAML template and subclassing `FrameworkAdapter`.

***

## Overview

Integrating a new framework requires two things:

1. **A YAML template** — defines how the framework's events map to Kyvvu's atomic Behavior vocabulary.
2. **A `FrameworkAdapter` subclass** — captures the framework's events and passes them through the template system.

## Step 1: Author a YAML template

> For a comprehensive guide on template authoring -- match semantics, property extraction, and a full worked example -- see the [Template Authoring Guide](/integrations/authoring).

The template maps framework events to behaviour fields (`step_type`, `verb`, `step_name`, `properties`). Rules are evaluated in order; first match wins. Each rule has a `match` block (conditions) and a `behavior` block (the emitted behaviour):

```yaml
name: my-framework-template
version: "1.0"

rules:
  - id: model_call
    match:
      event: "llm_invoke"
    behavior:
      step_type: "step.model"
      verb: "POST"
      step_name: "{{ name }}"
      properties:
        model:
          provider: "{{ provider }}"
          name: "{{ model_name }}"

  - id: tool_call
    match:
      event: "tool_execute"
    behavior:
      step_type: "step.resource"
      verb: "POST"
      step_name: "{{ name }}"

  - id: fallback
    match: {}
    behavior:
      step_type: "step.unknown"
```

Match conditions support `event` (exact match) and `name_pattern` (regex on the `name` context key). Template variables (`{{ provider }}`) are interpolated from the event context at match time. See [Writing a Custom Template](/integrations/custom-template) for the full match/behavior field reference.

### Loading and testing the template

```python
from kyvvu_engine.templates import BehaviorTemplate

template = BehaviorTemplate.from_path("my_framework.template.yaml")

# Test matching
result = template.match({
    "event": "llm_invoke",
    "name": "chat",
    "provider": "openai",
    "model_name": "gpt-4o",
})
assert result["step_type"] == "step.model"
assert result["properties"]["model"]["provider"] == "openai"
```

## Step 2: Subclass FrameworkAdapter

The `FrameworkAdapter` base class provides the machinery between your framework and the SDK. Set the `template_name` class attribute to the name of your built-in template, then write callback methods that build a context dict and delegate to the base class. The constructor takes an already-constructed `Kyvvu` instance (the adapter does not manage identity or registration) plus an optional `template` override:

```python
from kyvvu.integrations import FrameworkAdapter

class MyFrameworkAdapter(FrameworkAdapter):
    template_name = "myframework"  # resolves myframework.template.yaml

    def on_tool_event(self, tool_name: str, args: dict, run_id: str) -> None:
        """Called by your framework hook/callback."""
        context = {
            "event": "tool_execute",
            "name": tool_name,
            "task_id": self.kv.start_task() if ... else current_task_id,
        }
        # Two-phase emission: evaluate (pre-execution) then record (post).
        self._emit_step(context, input_data=args, step_run_id=run_id)
        # ... framework executes the tool ...
        self._record_step(run_id, output={"status": "success", "result": result})
```

### Base class methods

| Method                                           | Purpose                                                                                                                                                          |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `_emit_step(context, input_data, step_run_id)`   | Match the template, evaluate policies for the intended step, and buffer it. Raises `KyvvuBlockedError` on block (records the blocked step and flushes the task). |
| `_record_step(step_run_id, output=...)`          | Complete a step previously started by `_emit_step` and record it into history.                                                                                   |
| `_emit_task_start(context, task_id=None)`        | Emit `task.start` and return the task\_id.                                                                                                                       |
| `_emit_task_end(context, task_id=None)`          | Emit `task.end` and flush the task.                                                                                                                              |
| `_emit_task_error(context, error, task_id=None)` | Emit `task.error` and flush the task.                                                                                                                            |
| `_resolve_root(run_id, parent_run_id)`           | Resolve the root task\_id for a nested callback.                                                                                                                 |
| `_safe_end_task(task_id, emit_fn)`               | Idempotent, crash-safe task flush (re-raises `KyvvuBlockedError`).                                                                                               |

Subclasses may override the hooks `_build_properties(matched, context, step_run_id)` (inject framework-specific metadata before policy evaluation) and `_on_block(task_id, exc)` (circuit-breaker behaviour on block — CrewAI uses this because its event bus swallows exceptions).

### Integration with the framework

How you hook into the framework depends on the framework. Common patterns:

* **Callback/hook system** — register your adapter as a callback (like LangChain's `BaseCallbackHandler`).
* **Middleware** — insert the adapter as middleware in the framework's request pipeline.
* **Monkey-patching** — wrap the framework's core methods (least preferred).

## Step 3: Test the mapping

Write tests that verify your template produces the correct Behaviors for each framework event:

```python
from kyvvu_engine.templates import BehaviorTemplate

def test_llm_call_maps_to_step_model():
    template = BehaviorTemplate.from_path("my_framework.template.yaml")
    result = template.match({"event": "llm_invoke", "name": "chat", "model_name": "gpt-4o"})
    assert result["step_type"] == "step.model"
    assert result["verb"] == "POST"

def test_unknown_event_falls_back():
    template = BehaviorTemplate.from_path("my_framework.template.yaml")
    result = template.match({"event": "something_unexpected"})
    assert result["step_type"] == "step.unknown"
```

Test each event type your framework emits, including edge cases (nested events, error events, events with missing fields).

## Example: complete adapter

`template_name` is a class attribute; the constructor takes the `Kyvvu` instance. Each handler builds a context dict and drives the two-phase `_emit_step` / `_record_step` cycle (or the `_emit_task_*` lifecycle helpers):

```python
import uuid
from kyvvu import Kyvvu
from kyvvu.integrations import FrameworkAdapter

class MyFrameworkAdapter(FrameworkAdapter):
    template_name = "myframework"  # resolves myframework.template.yaml

    def on_kickoff(self) -> None:
        self._task_id = self._emit_task_start({"event": "kickoff", "name": "run"})

    def on_tool_use(self, tool_name: str, args: dict) -> None:
        run_id = str(uuid.uuid4())
        context = {"event": "tool_execute", "name": tool_name, "task_id": self._task_id}
        self._emit_step(context, input_data=args, step_run_id=run_id)
        result = ...  # framework executes the tool
        self._record_step(run_id, output={"status": "success", "result": result})

    def on_finish(self) -> None:
        self._safe_end_task(
            self._task_id,
            lambda: self._emit_task_end({"event": "finish", "name": "run"}, task_id=self._task_id),
        )
```

> The built-in CrewAI integration (`KyvvuCrewAIListener`, `kyvvu.integrations.crewai`) is a worked reference implementation of this pattern against the CrewAI event bus.

***

## Next steps

* [Templates](/core-concepts/templates) — deep-merge semantics and template loading options
* [Atomic Behaviours](/core-concepts/behaviours) — the 12 behaviour types to map to
* [REST API (Non-Python)](/integrations/rest-api) — for non-Python agents, use the HTTP server instead


# 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
kyvvu serve --host 127.0.0.1 --port 8080 --agent-key my-agent
```

The server needs API credentials to fetch policies. Provide them via flags or environment variables:

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

### 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
{
  "policy_count": 8,
  "last_fetch_at": "2026-04-29T10:00:00+00:00",
  "last_fetch_succeeded": true,
  "instance_id": "worker-3-a8f92",
  "ttl_remaining_seconds": 280.5
}
```

### 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",
      "scope": "step",
      "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 '{
    "agent_id": "agent-123",
    "task_id": "task-abc",
    "scope": "step",
    "step_type": "step.model",
    "verb": "POST",
    "step_name": "chat_gpt-4o",
    "input": {"user_message": "Hello"},
    "output": {"response": "Hi there!"}
  }'
```

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) — CLI reference for the serve command
* [Configuration Reference](/deployment/configuration) — all environment variables
* [Architecture](/core-concepts/architecture) — why in-process evaluation is preferred


# Creating Policies

**What you'll learn:** How to create policies via the dashboard and the API, how to scope them to specific agents, and how to enable/disable them.

***

## Via the dashboard

1. Navigate to **Policies** in the sidebar at [platform.kyvvu.com](https://platform.kyvvu.com).
2. Click **Create Policy**.
3. Fill in the form:
   * **Name** — a descriptive name (e.g. "No PII to external LLMs").
   * **Rule type** — select from the available rules. The form dynamically renders parameter fields based on the rule's schema.
   * **Parameters** — configure the rule (e.g. regex patterns, step types, field names).
   * **Scope** — `agent_registration` or `step_execution`.
   * **Severity** — `low`, `medium`, `high`, or `critical`.
   * **Agent** (optional) — scope to a specific agent, or leave blank for all agents.
   * **Risk classification** (optional) — scope to agents with this classification.
4. Click **Save**.

The policy takes effect within the policy TTL window (default: 5 minutes). No agent restart required.

## Via the API

```bash
curl -X POST https://platform.kyvvu.com/api/v1/policies \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "No PII to external LLMs",
    "rule_type": "pii_in_request",
    "params": {
      "patterns": ["\\d{3}-\\d{2}-\\d{4}", "\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}"]
    },
    "scope": "step_execution",
    "severity": "critical",
    "enabled": true
  }'
```

Response:

```json
{
  "id": 9,
  "name": "No PII to external LLMs",
  "rule_type": "pii_in_request",
  "params": {"patterns": ["\\d{3}-\\d{2}-\\d{4}", "..."]},
  "scope": "step_execution",
  "severity": "critical",
  "enabled": true,
  "agent_id": null,
  "risk_classification": null,
  "created_at": "2026-04-29T10:00:00+00:00"
}
```

### Scoping to an agent

```json
{
  "name": "Per-task $5 LLM budget",
  "rule_type": "usage_budget",
  "params": {"step_type": "step.model", "property_path": "usage.cost_usd", "budget": 5.0},
  "scope": "step_execution",
  "severity": "high",
  "agent_id": "ag_abc123"
}
```

### Scoping to a risk classification

```json
{
  "name": "High-risk agents: working hours only",
  "rule_type": "working_hours_only",
  "params": {"start_hour": 8, "end_hour": 18, "timezone": "Europe/Amsterdam"},
  "scope": "step_execution",
  "severity": "high",
  "risk_classification": "high"
}
```

## Listing available rules

Before creating a policy, you can list all available rule types (the building blocks for policies):

```bash
curl https://platform.kyvvu.com/api/v1/policies/rules \
  -H "Authorization: Bearer <JWT>"
```

Each rule includes its description, parameter schema, and applicable scopes.

> **Note:** `kyvvu list-policies` lists your *active policies* (instantiated rules with parameters), not the available rule types. Use the API endpoint above to discover what rules you can build policies from.

## Enabling and disabling

Policies can be toggled without deletion:

```bash
curl -X PUT https://platform.kyvvu.com/api/v1/policies/9 \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'
```

Disabled policies are skipped during evaluation. Re-enable by setting `enabled: true`.

## Deleting policies

```bash
curl -X DELETE https://platform.kyvvu.com/api/v1/policies/9 \
  -H "Authorization: Bearer <JWT>"
```

Deletion is a soft delete — the policy is marked as deleted but retained for audit purposes.

***

## Next steps

* [OWASP Default Template](/policy-authoring/owasp-default) — the 8 pre-built security policies
* [Compound Policies](/policy-authoring/compound) — combine rules with `all_of`, `any_of`, `not`
* [Built-in Rules Reference](/policy-authoring/rules-reference) — all 27 rules and their parameters


# OWASP Default Template

**What you'll learn:** The 8 OWASP security policies in the `owasp_agentic_default` manifest, what each one does, which OWASP risks they cover, and how to customize them.

***

## Overview

The OWASP Top 10 for Agentic Applications (2026) manifest provides baseline security coverage for any Kyvvu agent. It is available in the default Kyvvu/manifests repository and is the recommended starting point for all new agents. Assign it via the Manifests page or CLI (`kyvvu assign-manifest`).

Design principles:

* **Immediate value** — every policy works on install with zero configuration.
* **Structural over content-based** — catches missing gates, runaway loops, and undeclared tools. Does not require environment-specific patterns (regex, domains, budgets).
* **Demonstrates path-dependence** — policies 7 and 8 read task history, showcasing the "policies on paths" model.

## The 8 policies

### Registration policies (3)

| # | Name                                           | Rule                  | Severity | OWASP risks                             |
| - | ---------------------------------------------- | --------------------- | -------- | --------------------------------------- |
| 1 | Agent must declare a substantive purpose       | `field_matches_regex` | high     | ASI01, ASI10                            |
| 2 | Agent must declare a tool allowlist            | `field_not_empty`     | high     | ASI02, ASI04                            |
| 3 | Agent must declare a valid risk classification | `field_in_list`       | critical | (enables classification-aware policies) |

**Policy 1** requires the `purpose` field to be at least 30 characters (`^.{30,}$`). Prevents placeholder values like "chat" or "assistant". Without an explicit purpose, goal drift cannot be detected and hijacking has no baseline to violate.

**Policy 2** requires the `declared_tools` field to be non-empty. This is the precondition for runtime policy 4 (tool allowlist enforcement). Together they implement least-agency.

**Policy 3** requires `risk_classification` to be one of `high`, `limited`, or `minimal`. Bridges to EU AI Act Article 6 for installations that add compliance templates.

### Runtime policies (5)

| # | Name                                                             | Rule                     | Severity | OWASP risks         |
| - | ---------------------------------------------------------------- | ------------------------ | -------- | ------------------- |
| 4 | Tool calls must be in the declared allowlist                     | `step_name_in_allowlist` | critical | ASI02, ASI04        |
| 5 | Code execution requires a preceding gate                         | `step_requires_gate`     | critical | ASI05               |
| 6 | Destructive resource operations require a gate                   | `step_requires_gate`     | critical | ASI09               |
| 7 | External content taint: high-impact actions require a fresh gate | `not(all_of(...))`       | critical | ASI01, ASI06, ASI09 |
| 8 | Bound resource calls per task to prevent runaway loops           | `execution_max_steps`    | high     | ASI08, ASI02        |

**Policy 4** blocks tool calls whose `step_name` is not in the agent's `declared_tools` allowlist. Catches typosquatting, supply-chain swap-ins, and silent capability creep.

**Policy 5** requires any `step.exec` to be preceded by a `step.gate` (human approval, static check, or sandbox dry-run). Direct mitigation for OWASP ASI05 (Unexpected Code Execution).

**Policy 6** requires `step.resource` with verb `DELETE` to be preceded by a `step.gate`. Narrows to DELETE only so development iteration isn't constantly blocked. Production deployments typically widen to include POST and PATCH.

**Policy 7** is a compound path-dependent policy — the headline demonstration of "policies on paths". See below.

**Policy 8** blocks tasks that issue more than 50 `step.resource` calls. Catches runaway loops, the most common dev-time issue.

## Policy 7: the taint policy explained

This policy enforces: **if the agent has fetched external content earlier in this task, any subsequent high-impact action must be immediately preceded by a `step.gate`.**

It addresses the indirect prompt injection pattern (EchoLeak): malicious instructions hidden in retrieved content silently redirect the agent's plan.

The logic uses `not(all_of(...))`:

```
not(
  all_of(
    # Current step IS a high-impact action
    any_of(
      current_is(step.exec),
      current_is(step.message, POST),
      current_is(step.resource, POST),
      current_is(step.resource, PATCH),
      current_is(step.resource, DELETE)
    ),
    # External content HAS been fetched earlier
    history_contains(step.resource, GET, target.trust="external"),
    # Current step is NOT directly preceded by a gate
    not(step_directly_preceded_by(step.gate))
  )
)
```

Why `not(all_of(...))` instead of just `all_of(...)`? Rule functions return `True` to pass and `False` to block. `all_of` returns `True` when all conditions match. If all three danger conditions are present, that means the situation is dangerous — but `all_of` would return `True` (pass). The `not` wrapper inverts it to `False` (block). See [Compound Policies](/policy-authoring/compound) for a full explanation of this pattern.

Key property convention: `step.resource` GETs must set `target.trust = "external"` when fetching from outside an internal allowlist. The SDK's behavioural templates default this property based on URL/domain heuristics.

## OWASP coverage summary

| OWASP Risk                       | Coverage     | Policies                                                              |
| -------------------------------- | ------------ | --------------------------------------------------------------------- |
| ASI01 Agent Goal Hijack          | Strong       | 1, 7                                                                  |
| ASI02 Tool Misuse                | Strong       | 2, 4, 6, 8                                                            |
| ASI03 Identity & Privilege Abuse | Gap          | (add credential-taint policy when deploying `step.credential` events) |
| ASI04 Agentic Supply Chain       | Partial      | 2, 4                                                                  |
| ASI05 Unexpected Code Execution  | Strong       | 5                                                                     |
| ASI06 Memory & Context Poisoning | Partial      | 7                                                                     |
| ASI07 Insecure Inter-Agent Comms | Out of scope | (transport-layer concern, not runtime policy)                         |
| ASI08 Cascading Failures         | Moderate     | 8                                                                     |
| ASI09 Human-Agent Trust Exploit  | Strong       | 5, 6, 7                                                               |
| ASI10 Rogue Agents               | Partial      | 1                                                                     |

## What the demo agent demonstrates

The demo agent from `kyvvu init` runs three steps:

1. `step.model` (POST) — model call. Passes: no policies restrict model calls in the default template.
2. `step.resource` (GET) — resource read. Passes: reading is allowed.
3. `step.exec` — code execution. **Blocked**: no `step.gate` precedes it (policy 5).

This demonstrates that the same agent code is allowed or blocked depending on the task's history and the policies in effect.

## Customizing

### Disable a policy

```bash
curl -X PUT https://platform.kyvvu.com/api/v1/policies/{id} \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'
```

### Change severity

```bash
curl -X PUT https://platform.kyvvu.com/api/v1/policies/{id} \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{"severity": "high"}'
```

### Expansion path

The template deliberately excludes rules that require environment-specific configuration. The recommended additions, in order of coverage value:

| Priority | Add                                                               | Covers                                        | When                                                   |
| -------- | ----------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------ |
| 1        | Credential taint policy (`step_not_after` with `step.credential`) | ASI03                                         | Your deployment instruments `step.credential` events   |
| 2        | PII gate before model calls (`pii_in_request`)                    | ASI06, EU AI Act Art. 10                      | PII patterns are defined for your domain               |
| 3        | Domain allowlist for resource reads (`domain_allowlist`)          | Closes "what counts as external" for policy 7 | Internal/external domain split is known                |
| 4        | Cost ceiling (`usage_budget` over `usage.cost_usd`)               | ASI08 economic DoS                            | Cost properties are emitted on `step.model` behaviours |

***

## Next steps

* [Manifests](/policy-authoring/templates) — other available manifests (EU AI Act, data minimization)
* [Compound Policies](/policy-authoring/compound) — author your own `all_of` / `any_of` / `not` policies
* [Built-in Rules Reference](/policy-authoring/rules-reference) — all 27 rules


# Manifests

**What you'll learn:** What manifests are, how to connect a repository, and how to assign manifests to agents.

***

## What are manifests?

Manifests are YAML files that define sets of security policies. Instead of creating policies one by one, you connect a GitHub repository containing manifest files and assign them to your agents. Policies are created automatically from the manifest and evaluated by the engine.

Manifests live in a GitHub repository (e.g. [Kyvvu/manifests](https://github.com/Kyvvu/manifests)) and are versioned with Git. When a manifest is updated in the repo, you can resync the assignment to pick up changes.

## Connecting a repository

### Via the dashboard

1. Go to **Manifests** page.
2. Click **Add Repository**.
3. Enter the GitHub URL and a personal access token (optional for public repos).
4. Click **Connect**.

### Via the API

```bash
curl -X POST https://platform.kyvvu.com/api/v1/repos \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{"owner": "Kyvvu", "name": "manifests", "branch": "main", "token": "ghp_..."}'
```

## Assigning a manifest to an agent

### Via the dashboard

1. Go to **Manifests** page.
2. Click **Assign** on a manifest.
3. Select the agents to assign to.
4. Click **Save**.

### Via the CLI

```bash
kyvvu assign-manifest --agent-id <hash> --repo-id <id> --manifest owasp_agentic_default.yaml
```

### Via the API

```bash
curl -X POST https://platform.kyvvu.com/api/v1/assignments \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "<hash>", "repo_id": 1, "manifest_path": "owasp_agentic_default.yaml"}'
```

## Listing manifests and assignments

```bash
# List available manifests
kyvvu list-manifests

# List assignments (optionally filtered by agent)
kyvvu list-assignments --agent-id <hash>
```

## Resyncing assignments

When a manifest is updated in the repository, you can resync to pick up changes:

* **Dashboard:** Click the sync icon on the assignment in the Agents page.
* **API:** `POST /api/v1/assignments/{id}/resync`

Resync compares the Git SHA. If unchanged, it's a no-op. If changed, old policies are disabled and new ones created.

## Risk filtering

When assigning a manifest, policies with a `risk_classification` higher than the agent's risk level are automatically skipped. For example, a `high`-risk-only policy won't be applied to a `limited`-risk agent.

***

## Next steps

* [Creating Policies](/policy-authoring/creating) — create custom policies
* [Compound Policies](/policy-authoring/compound) — advanced policy authoring


# Compound Policies

**What you'll learn:** How to combine rules using `all_of`, `any_of`, and `not`, including the critical `not(all_of(...))` pattern for blocking triggers.

***

## Compound rules

Three compound rules let you combine conditions:

| Rule     | Passes when              | Use for                                    |
| -------- | ------------------------ | ------------------------------------------ |
| `all_of` | All sub-conditions pass  | Multiple requirements that must all be met |
| `any_of` | Any sub-condition passes | Alternative conditions, any one sufficient |
| `not`    | The sub-condition fails  | Inverting a condition                      |

Compound rules recurse freely: `all_of` can contain `any_of`, which can contain `not`, which can contain a primitive rule.

## The critical gotcha: `not(all_of(...))`

**Rule functions return `True` to pass and `False` to block.** This is the most common source of compound policy authoring mistakes.

If your intent is "block when conditions A, B, and C are all present," the naive approach is:

```json
{"rule_type": "all_of", "params": {"conditions": [A, B, C]}}
```

This is **wrong**. `all_of` returns `True` (passes) when all conditions are met. It would block every step where *any* condition is not met — the opposite of what you want.

The correct pattern:

```json
{
  "rule_type": "not",
  "params": {
    "condition": {
      "rule_type": "all_of",
      "params": {"conditions": [A, B, C]}
    }
  }
}
```

How it works:

* `all_of` returns `True` when all danger conditions are present (= dangerous situation detected).
* `not` inverts to `False` (= violated = blocked).
* If any condition is absent, `all_of` returns `False`, `not` inverts to `True` (= passes).

## Worked example: the taint policy

The OWASP default template's taint policy uses this pattern. It blocks high-impact actions when external content has entered the task and no fresh gate precedes the action.

```json
{
  "name": "External content taint: high-impact actions require a fresh gate",
  "rule_type": "not",
  "params": {
    "condition": {
      "rule_type": "all_of",
      "params": {
        "conditions": [
          {
            "rule_type": "any_of",
            "params": {
              "conditions": [
                {"rule_type": "current_is", "params": {"step_type": "step.exec"}},
                {"rule_type": "current_is", "params": {"step_type": "step.message", "verb": "POST"}},
                {"rule_type": "current_is", "params": {"step_type": "step.resource", "verb": "POST"}},
                {"rule_type": "current_is", "params": {"step_type": "step.resource", "verb": "PATCH"}},
                {"rule_type": "current_is", "params": {"step_type": "step.resource", "verb": "DELETE"}}
              ]
            }
          },
          {
            "rule_type": "history_contains",
            "params": {
              "step_type": "step.resource",
              "verb": "GET",
              "property_filter": {"target.trust": "external"}
            }
          },
          {
            "rule_type": "not",
            "params": {
              "condition": {
                "rule_type": "step_directly_preceded_by",
                "params": {"required_step_type": "step.gate"}
              }
            }
          }
        ]
      }
    }
  },
  "severity": "critical",
  "scope": "step_execution"
}
```

Reading the logic:

1. The outer `not` means: "block if the inner `all_of` passes."
2. The inner `all_of` checks three conditions:
   * Current step is a high-impact action (exec, outbound message, or mutating resource call).
   * External content has been fetched earlier in the task.
   * The current step is NOT directly preceded by a gate.
3. If all three are true → dangerous → `all_of` passes → `not` blocks.
4. If any is false → safe → `all_of` fails → `not` passes.

## Another example: PII + product data + model requires approval

"If the agent has read customer-data AND product-data AND called a model, then sending an outbound message requires a human-approval gate."

```json
{
  "name": "PII + product data + model requires human approval",
  "rule_type": "not",
  "params": {
    "condition": {
      "rule_type": "all_of",
      "params": {
        "conditions": [
          {"rule_type": "current_is",
           "params": {"step_type": "step.message", "verb": "POST"}},
          {"rule_type": "history_contains",
           "params": {"step_type": "step.resource", "verb": "GET",
                      "property_filter": {"target.table": "customer-data"}}},
          {"rule_type": "history_contains",
           "params": {"step_type": "step.resource", "verb": "GET",
                      "property_filter": {"target.table": "product-data"}}},
          {"rule_type": "history_contains",
           "params": {"step_type": "step.model"}},
          {"rule_type": "not",
           "params": {"condition": {"rule_type": "step_requires_gate",
                                    "params": {"target_step_types": ["step.message"],
                                               "target_verb": "POST",
                                               "gate_check_type": "human_approval"}}}}
        ]
      }
    }
  },
  "severity": "critical",
  "scope": "step_execution"
}
```

This policy wraps `all_of` in an outer `not`, exactly like the previous example. The inner `all_of` *passes* (returns `True`) when the dangerous situation holds — customer-data and product-data were both read, a model was called, the current step is an outbound message, and **no** human-approval gate exists (the inner `not(step_requires_gate)` is `True` precisely when the gate is absent). Because a rule that returns `True` means *compliant*, that dangerous combination must be inverted by the outer `not` so it surfaces as a violation. The `explain()` output below shows this structure: `not → FAIL (inner passed)`.

## Debugging compound policies

Use the engine's `explain()` method to see per-node pass/fail:

```python
print(engine.explain(intended, context))
```

```
Evaluated 1 policy for step.message/POST "send_reply" (task=task-abc step=5):
  ✗ External content taint           (critical) FAILED
    not → FAIL (inner passed)
      all_of → pass (3/3 conditions met)
        any_of → pass (1/5 matched: current_is step.message/POST)
        history_contains → pass (step.resource/GET target.trust=external found)
        not → pass (inner failed)
          step_directly_preceded_by → FAIL (previous was step.model, not step.gate)
→ action=block (risk_score=1.00)
```

Incidents from compound policies carry the full condition tree in `violation_details`.

***

## Next steps

* [OWASP Default Template](/policy-authoring/owasp-default) — see compound policies in context
* [Built-in Rules Reference](/policy-authoring/rules-reference) — all primitive rules you can compose
* [Creating Policies](/policy-authoring/creating) — create and deploy your compound policy


# Built-in Rules Reference

**What you'll learn:** All 27 built-in rule functions, grouped by category, with their parameters and applicable scopes.

***

The engine ships with 27 built-in rules grouped into six categories. Each rule is a pure function: `rule(data, params, context) -> bool`. Returns `True` to pass, `False` to violate.

## Field rules

Applicable to `agent_registration` and `step_execution`.

| Rule                  | What it checks                          | Parameters                        |
| --------------------- | --------------------------------------- | --------------------------------- |
| `field_not_empty`     | Named field has a non-empty value.      | `field: str`                      |
| `field_in_list`       | Named field's value is in an allowlist. | `field: str`, `values: list[str]` |
| `field_matches_regex` | Named field matches a regex pattern.    | `field: str`, `pattern: str`      |

## Path rules

Require task history. `step_execution` only.

| Rule                                   | What it checks                                                                                                                                                                                     | Parameters                                                                                                                                                              |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `step_directly_preceded_by`            | The immediately preceding step in history is the required type.                                                                                                                                    | `required_step_type: str`, `target_step_types: list[str]` (optional), `target_verb: str` (optional), `target_property_filter: dict` (optional)                          |
| `step_requires_predecessor`            | Some earlier step in history has the required type.                                                                                                                                                | `required_step_type: str`, `target_step_types: list[str]`, `target_verb: str` (optional), `target_property_filter: dict` (optional)                                     |
| `step_preceded_by_without_intervening` | A required predecessor exists with no forbidden steps between.                                                                                                                                     | `required_step_type: str`, `forbidden_intervening: list[str]`, `target_step_types: list[str]`, `target_verb: str` (optional), `target_property_filter: dict` (optional) |
| `step_requires_dedicated_predecessor`  | Each target step has its own unconsumed predecessor — one predecessor cannot authorise two target steps.                                                                                           | `required_step_type: str`, `target_step_types: list[str]`, `target_verb: str` (optional), `target_property_filter: dict` (optional)                                     |
| `step_requires_gate`                   | A matching `step.gate` precedes this step. The gate may be any distance earlier in history (no freshness enforcement).                                                                             | `target_step_types: list[str]`, `target_verb: str` (optional), `gate_check_type: str` (optional), `gate_result: str` (optional, default `pass`)                         |
| `sequence_forbidden`                   | A forbidden ordered sequence (subsequence over history + current step) has not occurred.                                                                                                           | `forbidden_sequence: list[str]`                                                                                                                                         |
| `step_not_after`                       | This step type is forbidden once a forbidden predecessor type has occurred (permanently tainted).                                                                                                  | `target_step_types: list[str]`, `forbidden_predecessor_step_types: list[str]`, `target_verb: str` (optional)                                                            |
| `path_within_root`                     | Allowlist containment: the target path resolves at or under the session `project_root`. Lexical only — `..` is collapsed but symlinks are not resolved; fails open when `project_root` is unknown. | `field: str` (optional, default `target.host`), `target_step_types: list[str]` (optional), `target_verb: str` (optional), `target_verbs: list[str]` (optional)          |
| `history_contains`                     | The history contains a step matching type + optional verb + optional property filter.                                                                                                              | `step_type: str`, `verb: str` (optional), `property_filter: dict` (optional)                                                                                            |
| `current_is`                           | The intended behaviour matches type + optional verb + optional property filter.                                                                                                                    | `step_type: str`, `verb: str` (optional), `property_filter: dict` (optional)                                                                                            |

## Count rules

| Rule                         | What it checks                                                                                               | Scopes           | Parameters                                                                                    |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------- | --------------------------------------------------------------------------------------------- |
| `execution_max_steps`        | Task has not exceeded a maximum count of the given step type.                                                | `step_execution` | `step_type: str`, `max_steps: int`, `verb: str` (optional)                                    |
| `max_consecutive_same_type`  | No run of the same step type exceeds a limit.                                                                | `step_execution` | `step_type: str`, `max_consecutive: int`                                                      |
| `cross_execution_rate_limit` | Agent has not exceeded N of this step\_type in the last M minutes across tasks.                              | `step_execution` | `step_type: str`, `max_count: int`, `window_minutes: int`, `property_filter: dict` (optional) |
| `usage_budget`               | Cumulative usage metric across task history has not exceeded budget. The first occurrence is always allowed. | `step_execution` | `step_type: str`, `property_path: str`, `budget: float`                                       |

## Classification rules

| Rule                                | What it checks                                                                          | Scopes           | Parameters                                                                                                                                       |
| ----------------------------------- | --------------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `step_forbidden_for_classification` | This step type is not permitted for the agent's risk classification.                    | `step_execution` | `forbidden_step_type: str`, `agent_risk_classifications: list[str]`, `forbidden_verb: str` (optional), `target_property_filter: dict` (optional) |
| `working_hours_only`                | Current time is within a permitted window. Supports overnight wraparound and timezones. | `step_execution` | `start_hour: int`, `end_hour: int`, `timezone: str` (optional, default UTC)                                                                      |
| `step_name_in_allowlist`            | This step's name is in the agent's declared tool allowlist.                             | `step_execution` | `agent_field: str`, `target_step_types: list[str]` (optional)                                                                                    |

## Content rules

| Rule               | What it checks                                                                                                                                                            | Scopes           | Parameters                   |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ---------------------------- |
| `pii_in_request`   | Step input does not contain PII matching configured regex patterns. Patterns are required; no defaults. Input is serialised via `json.dumps` so nested dicts are scanned. | `step_execution` | `patterns: list[str]`        |
| `domain_allowlist` | Step's target domain is in an allowlist.                                                                                                                                  | `step_execution` | `allowed_domains: list[str]` |

## Flow rules

| Rule                             | What it checks                                                                                                  | Scopes           | Parameters                                                                                                                                                    |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `conditional_successor_required` | If the previous step matches a trigger type and condition, the current step must satisfy successor constraints. | `step_execution` | `trigger_step_types: list[str]`, `trigger_condition: dict`, `required_step_type: str` (optional), `forbidden_step_types: list[str]` (optional)                |
| `tainted_path_block`             | If any prior step matches the taint pattern, certain downstream steps are blocked.                              | `step_execution` | `taint_step_type: str`, `target_step_types: list[str]`, `taint_verb: str` (optional), `taint_property_filter: dict` (optional), `target_verb: str` (optional) |
| `all_of`                         | Compound: passes iff all sub-conditions pass.                                                                   | Both             | `conditions: list[condition]`                                                                                                                                 |
| `any_of`                         | Compound: passes iff any sub-condition passes.                                                                  | Both             | `conditions: list[condition]`                                                                                                                                 |
| `not`                            | Compound: passes iff the sub-condition fails.                                                                   | Both             | `condition: condition`                                                                                                                                        |

Each sub-condition in compound rules is a dict with `rule_type` and `params` keys.

## Programmatic discovery

Rules are self-documenting. To list all rules programmatically:

```python
from kyvvu_engine import PolicyRule

# All rules for a given scope
rules = PolicyRule.get_all_rules(scope="step_execution")
# → {"field_not_empty": {"description": "...", "scopes": [...], "params_schema": {...}}, ...}

# Validate a rule exists
valid, error = PolicyRule.validate_rule_params("pii_in_request", {"patterns": ["\\d{3}-\\d{2}-\\d{4}"]})
```

The platform dashboard uses this metadata to render dynamic forms when creating policies.

***

## Next steps

* [Creating Policies](/policy-authoring/creating) — use these rules to author policies
* [Compound Policies](/policy-authoring/compound) — combine rules with `all_of`, `any_of`, `not`
* [OWASP Default Template](/policy-authoring/owasp-default) — see 8 of these rules in action


# Dashboard Guide

**What you'll learn:** How to navigate the Kyvvu dashboard, oversee agents, and manage policies.

***

## Overview

The Kyvvu dashboard is a web interface at [platform.kyvvu.com](https://platform.kyvvu.com) for overseeing agents, managing policies, triaging incidents, and generating reports.

## Navigation

The sidebar provides access to all sections:

| Section       | What it shows                                                                                               |
| ------------- | ----------------------------------------------------------------------------------------------------------- |
| **Dashboard** | Overview stats (agents, manifests, policies, incidents), recent incidents table, recent activity.           |
| **Agents**    | Agent list with risk, environment, last policy fetch. Expand for assigned manifests, resync, view policies. |
| **Manifests** | Connect GitHub repos, browse YAML manifest files, assign manifests to agents via checkbox modal.            |
| **Logs**      | Behavioral trace logs — searchable by agent, task ID, time range. Hash chain validation.                    |
| **Incidents** | Policy violations with severity, status, and agent details.                                                 |
| **Settings**  | Sub-tabs: API Keys, Platform Events, Reports, Organization, Members.                                        |

## Agent oversight

The **Agents** page lists all registered agents with:

* Agent key and ID
* Name and purpose
* Environment (development / staging / production)
* Risk classification (high / limited / minimal)
* Active/inactive status

Click an agent to see its details, associated policies, recent logs, and incidents.

## Policy management

Policies are created automatically when manifests are assigned to agents. To manage policies:

* **Manifests page**: Connect a repo, browse manifests, assign to agents
* **Agents page**: Expand an agent to see assigned manifests, view policies, update or remove assignments

## Log viewer

The **Logs** page displays the behavioral trace as a paginated list of tasks:

* The task list is loaded server-side (`GET /api/v1/logs/tasks`), one summary row per task with its agent, environment, step count, and status — 20 tasks per page. This scales to agents with hundreds of steps per task.
* Filter by agent or time range; the free-text task ID filter resolves a single task directly.
* Expand a task (or open its details) to lazy-load the full ordered sequence of steps — steps are fetched on demand, not up front.
* Each step shows its evaluation result (allow / warn / block) and the Behavior data.
* Hash chain validation: click "Validate" on a task to verify chain integrity.

## Authentication

Login at [platform.kyvvu.com](https://platform.kyvvu.com) with your email and password. JWT tokens expire after 24 hours.

***

## Next steps

* [Incident Management](/platform/incidents) — triage and resolve policy violations
* [Organizations & Access](/platform/organizations) — manage team members and permissions
* [Reports](/platform/reports) — generate compliance reports


# Incident Management

**What you'll learn:** How incidents are created, the incident lifecycle, and how to triage and resolve them.

***

## What is an incident?

An **incident** is a policy violation record. When the engine evaluates a step and a policy returns `warn` or `block`, an incident is created and sent to the configured incident sink (by default, the same destination as the behavioral trace — see [Configuring the incident sink](#configuring-the-incident-sink)).

Incidents provide the audit trail — they record what was violated, when, by which agent, and in which task.

## Incident lifecycle

```
open  →  active  →  resolved
                 →  ignored
```

| Status       | Meaning                                                                                  |
| ------------ | ---------------------------------------------------------------------------------------- |
| **open**     | Newly created. No human has reviewed it.                                                 |
| **active**   | Acknowledged by a team member. Under investigation or remediation.                       |
| **resolved** | The violation has been addressed — code fixed, policy adjusted, or root cause mitigated. |
| **ignored**  | Reviewed and determined to be a false positive or acceptable risk.                       |

## Incident data

Each incident includes:

| Field        | Description                                                     |
| ------------ | --------------------------------------------------------------- |
| `agent_id`   | The agent that triggered the violation.                         |
| `scope`      | `step_execution` or `agent_registration`.                       |
| `task_id`    | The task in which the violation occurred (for step violations). |
| `step_name`  | The step that was evaluated.                                    |
| `step_type`  | The behaviour type.                                             |
| `action`     | `warn` or `block`.                                              |
| `risk_score` | The aggregate risk score (0.0 to 1.0).                          |
| `violations` | List of violated policies with name, severity, and details.     |
| `timestamp`  | When the violation occurred.                                    |

## Dashboard workflow

1. Navigate to **Incidents** in the sidebar.
2. Filter by status, agent, severity, or time range.
3. Click an incident to see full details.
4. Use the action buttons:
   * **Resolve** — mark as addressed.
   * **Ignore** — mark as false positive.
   * **Reactivate** — reopen a resolved or ignored incident.

## API endpoints

| Method | Path                                | Description                  |
| ------ | ----------------------------------- | ---------------------------- |
| `GET`  | `/api/v1/incidents`                 | List incidents (filterable). |
| `GET`  | `/api/v1/incidents/{id}`            | Get incident details.        |
| `PUT`  | `/api/v1/incidents/{id}/resolve`    | Resolve incident.            |
| `PUT`  | `/api/v1/incidents/{id}/ignore`     | Ignore incident.             |
| `PUT`  | `/api/v1/incidents/{id}/reactivate` | Reactivate incident.         |

Incidents are never deleted — they form part of the immutable audit trail.

## Configuring the incident sink

Incidents follow the same **location + format** sink model as behavioral traces. By default the incident sink **inherits the trace sink** (`KV_LOG_LOCATION` / `KV_LOG_FORMAT`), substituting the `…/api/v1/incidents` path for the Kyvvu `kv` format. So if traces already flow to the platform, incidents do too — no extra configuration needed.

To route incidents to a **different** destination, set `KV_INCIDENT_LOCATION` (and optionally `KV_INCIDENT_FORMAT`):

```bash
# Send incidents to the platform explicitly (kv format → …/api/v1/incidents)
export KV_INCIDENT_LOCATION=https://platform.kyvvu.com
export KV_INCIDENT_FORMAT=kv
```

`KV_INCIDENT_LOCATION` accepts the same values as `KV_LOG_LOCATION` — an HTTP URL, a file path (JSONL), `stdout`, or `none`/empty to disable:

```bash
# Local debugging — print incident JSON to stdout
export KV_INCIDENT_LOCATION=stdout
```

```bash
# Emit a standalone kyvvu.incident OpenTelemetry span
export KV_INCIDENT_LOCATION=http://collector:4318
export KV_INCIDENT_FORMAT=otlp
```

***

## Next steps

* [Dashboard Guide](/platform/dashboard) — navigate the dashboard
* [Reports](/platform/reports) — generate audit reports from incident data
* [Policies and Rules](/core-concepts/policies) — understand how violations are determined


# Organizations & Access

**What you'll learn:** How organizations, members, and access control work in the Kyvvu platform.

***

## Organizations

Every Kyvvu account belongs to an organization. All data (agents, policies, logs, incidents) is scoped to the authenticated user's organization. There is no cross-organization data access.

When you register with `kyvvu register`, your organization is determined by your email domain. Users with the same email domain are grouped into the same organization.

## Member roles

| Role       | Capabilities                                                                          |
| ---------- | ------------------------------------------------------------------------------------- |
| **Admin**  | Full access: manage members, create/delete agents, manage policies, view all data.    |
| **Member** | Create agents, view data, manage own API keys. Cannot manage org settings or members. |

## Member verification

New members require verification before they can create agents or API keys:

1. User registers → account created, marked as **pending**.
2. An org admin verifies the member via the dashboard or API.
3. Once verified, the member can create agents and API keys.

If you registered and see "pending verification," contact your org admin.

### Registration cap

The platform has an early-access registration cap (`KV_REGISTRATION_CAP`). When the cap is reached, new accounts are created but **waitlisted**. Waitlisted users receive a message:

```
Kyvvu is in early access — we're actively managing growth.
Your account has been created and you're on the waitlist.
We'll contact you at you@company.com when your account is ready.
```

## Dashboard management

1. Navigate to **Organization** in the sidebar.
2. **Members** tab — view all members, their roles, and verification status.
3. Click a member to:
   * **Verify** — approve a pending member.
   * **Change role** — promote to admin or demote to member.
   * **Remove** — revoke access.

## API endpoints

| Method   | Path                                      | Description               |
| -------- | ----------------------------------------- | ------------------------- |
| `GET`    | `/api/v1/orgs`                            | List organizations.       |
| `GET`    | `/api/v1/orgs/{org_id}`                   | Get organization details. |
| `GET`    | `/api/v1/orgs/{org_id}/members`           | List members.             |
| `POST`   | `/api/v1/orgs/{org_id}/members`           | Invite a member.          |
| `PATCH`  | `/api/v1/orgs/{org_id}/members/{user_id}` | Update role or verify.    |
| `DELETE` | `/api/v1/orgs/{org_id}/members/{user_id}` | Remove member.            |
| `GET`    | `/api/v1/orgs/{org_id}/stats`             | Organization statistics.  |

## API keys

API keys authenticate agents with the platform. Manage them via the dashboard (**Organization** > **API Keys**) or the API:

| Method   | Path                             | Description                 |
| -------- | -------------------------------- | --------------------------- |
| `POST`   | `/api/v1/auth/api-keys`          | Create a new API key.       |
| `GET`    | `/api/v1/auth/api-keys`          | List API keys.              |
| `PATCH`  | `/api/v1/auth/api-keys/{key_id}` | Enable/disable a key.       |
| `DELETE` | `/api/v1/auth/api-keys/{key_id}` | Revoke (soft-delete) a key. |

API keys are prefixed with `KvKey-` and are shown only once at creation time.

***

## Next steps

* [Dashboard Guide](/platform/dashboard) — navigate the platform
* [Incident Management](/platform/incidents) — triage policy violations
* [Configuration Reference](/deployment/configuration) — environment variables including registration cap


# Reports

**What you'll learn:** How to generate compliance audit reports from the Kyvvu platform.

***

## Audit reports

The platform generates audit reports that summarize agent activity, policy compliance, and incident history. Reports are available in PDF and XML formats.

## Generating a report

### Via the dashboard

1. Navigate to **Reports** in the sidebar.
2. Select the report parameters:
   * **Format** — PDF or XML.
   * **Time range** — start and end dates.
   * **Agent** (optional) — scope to a specific agent.
3. Click **Generate**.
4. Download the report when ready.

### Via the API

```bash
curl "https://platform.kyvvu.com/api/v1/reports/audit?format=pdf&start_date=2026-04-01&end_date=2026-04-30" \
  -H "Authorization: Bearer <JWT>" \
  -o audit-report.pdf
```

Query parameters:

| Parameter    | Type           | Required | Description                |
| ------------ | -------------- | -------- | -------------------------- |
| `format`     | `pdf` or `xml` | Yes      | Output format.             |
| `start_date` | ISO date       | No       | Start of reporting period. |
| `end_date`   | ISO date       | No       | End of reporting period.   |
| `agent_id`   | string         | No       | Scope to a specific agent. |

## Report contents

Reports include:

* **Agent summary** — registered agents, their classifications, and environments.
* **Policy summary** — active policies, their scopes and severities.
* **Incident summary** — violations grouped by severity, status, and agent.
* **Activity summary** — log volume, step type distribution, task counts.
* **Hash chain validation** — integrity status of the behavioral trace.

XML reports use a structured schema suitable for automated compliance tooling and regulatory submission.

***

## Next steps

* [Incident Management](/platform/incidents) — the incident data that feeds reports
* [Dashboard Guide](/platform/dashboard) — navigate the platform
* [Organizations & Access](/platform/organizations) — access control for reports


# kyvvu register / login

**What you'll learn:** How to create an account, log in, check your session, and manage authentication.

***

## `kyvvu register`

Create a new Kyvvu account. Prompts for email, display name, and password. On success, creates an API key and stores it locally.

```
Usage: kyvvu register [OPTIONS]

Options:
  --api-url TEXT  Kyvvu API URL (default: https://platform.kyvvu.com)
```

### Example

```bash
$ kyvvu register
Registering at https://platform.kyvvu.com

Email: you@company.com
Display name: Your Name
Password:
Confirm password:

✓ Account created and verified.

Your API key (save this — it will not be shown again):

    KvKey-abc123...

Set it in your environment:

    export KV_API_KEY=KvKey-abc123...

Next steps:
    kyvvu init my-agent
```

### Verification and waitlisting

If the platform is at capacity, your account is created but waitlisted:

```
Kyvvu is in early access — we're actively managing growth.
Your account has been created and you're on the waitlist.
We'll contact you at you@company.com when your account is ready.
```

If your organization requires admin approval, you'll see:

```
Your account at company.com is pending verification.
An admin at your organization needs to approve you before you can
create agents or API keys.
Once they've approved you, run `kyvvu login` to refresh your session.
```

## `kyvvu login`

Log in to an existing account. Stores a JWT locally for dashboard and CLI operations.

```
Usage: kyvvu login [OPTIONS]

Options:
  --api-url TEXT  Kyvvu API URL
```

### Example

```bash
$ kyvvu login
Email: you@company.com
Password:

✓ Logged in as you@company.com
```

If no API key is stored, you'll be offered to create one.

## `kyvvu forgot-password`

Request a password reset email for your account.

```
Usage: kyvvu forgot-password [OPTIONS]

Options:
  --api-url TEXT  Kyvvu API URL
```

## `kyvvu logout`

Clear the JWT session. Your API key in `~/.kyvvu/config.toml` is preserved — agents continue to work.

```bash
$ kyvvu logout
✓ Logged out. Your API key in ~/.kyvvu/config.toml is preserved
(the SDK and agents continue to work).
```

## `kyvvu whoami`

Show current user info, verification status, and endpoint configuration.

```bash
$ kyvvu whoami
Email:         you@company.com
API URL:       https://platform.kyvvu.com
Verification:  ✓ verified
Log location:  stdout
Incidents:     (inherits log location: stdout)
```

## Configuration storage

Auth state is stored in `~/.kyvvu/config.toml`:

* JWT token and expiry (cleared by `kyvvu logout`)
* Default API key (preserved across logins)
* API URL

***

## Next steps

* [kyvvu init](/cli-reference/init) — scaffold a new agent project
* [Installation](/getting-started/installation) — full setup guide


# kyvvu init

**What you'll learn:** How `kyvvu init` scaffolds a new agent project.

***

## Usage

```
Usage: kyvvu init [PROJECT_NAME]

Arguments:
  PROJECT_NAME  Project directory name (optional; uses current directory if omitted)
```

## What it does

Scaffolds project files — no authentication required:

* `agent.py` — demo agent with three decorated steps
* `requirements.txt` — `kyvvu` dependency
* `.env.example` — environment variable template
* `.gitignore` — standard Python gitignore
* `README.md` — project documentation

Policies are assigned separately via the dashboard (Manifests page) or CLI (`kyvvu assign-manifest`).

## Examples

### New directory

```bash
$ kyvvu init my-agent
✓ Created my-agent/

Next steps:

    cd my-agent

    # Install requirements:
    pip install -r requirements.txt

    # Set your key:
    # (run `kyvvu register` if you have none)
    cp .env.example .env       # add your KV_API_KEY
    # or: export KV_API_KEY=your-key

    # Run the agent:
    python agent.py

To assign security policies, go to the dashboard:
    Manifests page → select a manifest → assign to your agent
Or use the CLI:
    kyvvu list-manifests
    kyvvu assign-manifest --agent-id <id> --repo-id <id> --manifest <path>
```

### Current directory

```bash
$ mkdir my-project && cd my-project
$ kyvvu init
```

If the directory is not empty, you'll be asked to confirm.

***

## Next steps

* [Your First Agent](/getting-started/first-agent) — run the demo agent and understand the output
* [Manifests](/policy-authoring/templates) — assign security policies to your agent
* [kyvvu serve](/cli-reference/serve) — run a local policy evaluation server


# kyvvu serve

**What you'll learn:** How to run a local policy evaluation server for non-Python agents.

***

## Usage

```
Usage: kyvvu serve [OPTIONS]

Options:
  --host TEXT       Bind address (default: 127.0.0.1)
  --port INTEGER    Bind port (default: 8080)
  --api-url TEXT    Kyvvu platform API URL
  --api-key TEXT    Bearer API key
  --agent-key TEXT  Agent key for policy fetch
```

## What it does

Starts a local HTTP server that wraps the kyvvu-engine. 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.

## Prerequisites

Requires `uvicorn` and `fastapi`. These are included when you install the engine with serve extras:

```bash
pip install "kyvvu-engine[serve]"
```

Or install the full SDK (which includes everything):

```bash
pip install kyvvu
```

## Starting the server

```bash
$ 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
  Endpoints: http://127.0.0.1:8080/evaluate, /record, /end_task, /health
```

Or with explicit flags:

```bash
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 policy count, last fetch time, TTL remaining.                            |
| `POST` | `/evaluate`       | Preflight evaluation of an intended behaviour. Returns `{action, risk_score, policies, blocked}`. |
| `POST` | `/record`         | Record a completed step. Returns `{step, task_id}`.                                               |
| `POST` | `/end_task`       | Close a task — evict history and flush logs. Returns `{status, task_id}`.                         |
| `POST` | `/register_agent` | Evaluate agent-registration policies.                                                             |

See [REST API (Non-Python)](/integrations/rest-api) for full request/response examples.

## Configuration

All `KV_*` environment variables work identically to `KyvvuRunner`:

* `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, or `none`)
* `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 `~/.kyvvu/config.toml` (populated by `kyvvu login`).

***

## Next steps

* [REST API (Non-Python)](/integrations/rest-api) — integration guide with curl examples
* [Configuration Reference](/deployment/configuration) — all environment variables
* [Architecture](/core-concepts/architecture) — why in-process evaluation is preferred over remote


# kyvvu list-agents / list-policies / manifests

**What you'll learn:** How to inspect agents, policies, manifests, and assignments from the command line.

***

## `kyvvu list-agents`

List all registered agents in your organization.

```
Usage: kyvvu list-agents [OPTIONS]

Options:
  --environment TEXT    Filter by environment (development, staging, production)
  --include-inactive    Include inactive (deactivated) agents
```

### Example

```bash
$ kyvvu list-agents
 AGENT_KEY        AGENT_ID     NAME              ENVIRONMENT    ACTIVE
 hello-kyvvu      ag_abc123    Hello Kyvvu       development    ✓
 gmail-assistant   ag_def456    Gmail Assistant    production     ✓
```

Requires an active JWT session (`kyvvu login`).

***

## `kyvvu list-policies`

List policies for your organization, optionally filtered by agent.

```
Usage: kyvvu list-policies [OPTIONS]

Options:
  --agent-id TEXT         Filter by agent ID
  --scope TEXT            Filter by scope (agent_registration, step_execution)
  --enabled / --disabled  Filter by enabled state
```

### Example

```bash
$ kyvvu list-policies --agent-id ag_abc123
 NAME                                              SCOPE               RULE_TYPE              SEVERITY  SOURCE                          ENABLED
 Agent must declare a substantive purpose           agent_registration  field_matches_regex    high      owasp_agentic_default.yaml      ✓
 Tool calls must be in the declared allowlist       step_execution      step_name_in_allowlist critical  owasp_agentic_default.yaml      ✓
```

The **Source** column shows which manifest the policy came from (or `-` for manually created policies).

Requires an active JWT session (`kyvvu login`).

***

## `kyvvu list-manifests`

List available manifests from connected GitHub repositories.

```
Usage: kyvvu list-manifests [OPTIONS]

Options:
  --repo-id INTEGER    Filter by repository ID
```

### Example

```bash
$ kyvvu list-manifests
 MANIFEST                                FILE                          SOURCE            REPO ID  POLICIES  VALID
 OWASP Top 10 for Agentic Applications   owasp_agentic_default.yaml    Kyvvu/manifests   1        8         ✓
 EU AI Act - Basic                        eu_ai_act_basic.yaml          Kyvvu/manifests   1        6         ✓
```

If no repositories are connected, you'll see a message directing you to the dashboard.

Requires an active JWT session (`kyvvu login`).

***

## `kyvvu assign-manifest`

Assign a manifest to an agent, creating policies automatically.

```
Usage: kyvvu assign-manifest [OPTIONS]

Options:
  --agent-id TEXT    Agent identifier (required)
  --repo-id INTEGER  Repository ID (required)
  --manifest TEXT    Manifest file path in the repo (required)
```

### Example

```bash
$ kyvvu assign-manifest --agent-id ag_abc123 --repo-id 1 --manifest owasp_agentic_default.yaml
✓ Assigned 'OWASP Top 10 for Agentic Applications' to agent ag_abc123 (8 policies created)
```

Risk filtering is applied automatically — policies with a risk classification higher than the agent's level are skipped.

Requires an active JWT session (`kyvvu login`).

***

## `kyvvu list-assignments`

List manifest assignments, optionally filtered by agent.

```
Usage: kyvvu list-assignments [OPTIONS]

Options:
  --agent-id TEXT    Filter by agent ID
```

### Example

```bash
$ kyvvu list-assignments
 AGENT        MANIFEST                                POLICIES  SHA      ASSIGNED AT
 ag_abc123    OWASP Top 10 for Agentic Applications   8         3523abc  2026-05-14
 ag_def456    EU AI Act - Basic                        6         f91de02  2026-05-14
```

Requires an active JWT session (`kyvvu login`).

***

## `kyvvu unassign-manifest`

Remove a manifest assignment (and the policies it created) from an agent.

```
Usage: kyvvu unassign-manifest [OPTIONS]

Options:
  --assignment-id INTEGER  Assignment ID from list-assignments (required)
```

### Example

```bash
$ kyvvu unassign-manifest --assignment-id 3
```

Requires an active JWT session (`kyvvu login`).

***

## Other CLI commands

| Command                 | Description                    | Reference                     |
| ----------------------- | ------------------------------ | ----------------------------- |
| `kyvvu register`        | Create a new account           | [Auth](/cli-reference/auth)   |
| `kyvvu login`           | Log in                         | [Auth](/cli-reference/auth)   |
| `kyvvu forgot-password` | Request a password reset email | [Auth](/cli-reference/auth)   |
| `kyvvu logout`          | Log out                        | [Auth](/cli-reference/auth)   |
| `kyvvu whoami`          | Show current user              | [Auth](/cli-reference/auth)   |
| `kyvvu init`            | Scaffold a project             | [Init](/cli-reference/init)   |
| `kyvvu serve`           | Start local engine server      | [Serve](/cli-reference/serve) |
| `kyvvu --version`       | Print version                  | —                             |

***

## Next steps

* [Manifests](/policy-authoring/templates) — manifest structure and assignment workflow
* [Dashboard Guide](/platform/dashboard) — visual interface for the same data


# Configuration Reference

**What you'll learn:** All environment variables for the SDK, engine, and platform API.

***

## SDK / Engine configuration

Precedence (highest to lowest): explicit kwargs > environment variables > `.env` in cwd > built-in defaults.

### Authentication and identity

| Env var          | Default                      | Description                                                                             |
| ---------------- | ---------------------------- | --------------------------------------------------------------------------------------- |
| `KV_API_URL`     | `https://platform.kyvvu.com` | Base URL of the Kyvvu platform API.                                                     |
| `KV_API_KEY`     | --                           | Bearer API key (`KvKey-...`). Required for policy fetch.                                |
| `KV_AGENT_KEY`   | --                           | Stable agent identifier used to fetch policies.                                         |
| `KV_INSTANCE_ID` | auto-generated               | Identifier for this runner instance. A random suffix is appended to prevent collisions. |

### Log output

| Env var                | Default                         | Description                                                                                                                                       |
| ---------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `KV_LOG_LOCATION`      | `stdout`                        | WHERE logs go. URL → HTTP POST, file path → JSONL, `stdout` → terminal, `none`/empty → disabled.                                                  |
| `KV_LOG_FORMAT`        | `kv`                            | HOW logs are formatted: `kv` (Kyvvu batch API), `json`, or `otlp`.                                                                                |
| `KV_INCIDENT_LOCATION` | unset (inherit trace sink)      | WHERE incidents go. Same vocabulary as `KV_LOG_LOCATION`. Unset inherits the trace sink, using the `…/api/v1/incidents` path for the `kv` format. |
| `KV_INCIDENT_FORMAT`   | unset (inherit `KV_LOG_FORMAT`) | HOW incidents are formatted: `kv`, `json`, or `otlp` (a standalone `kyvvu.incident` span).                                                        |

### Behaviour

| Env var                | Default      | Description                                                                                     |
| ---------------------- | ------------ | ----------------------------------------------------------------------------------------------- |
| `KV_ENVIRONMENT`       | `production` | Forwarded to `EvalContext.environment`.                                                         |
| `KV_LOG_PAYLOADS`      | `full`       | `full` includes step input/output in logs. `metadata_only` redacts content but preserves shape. |
| `KV_TEMPLATE_LOCATION` | built-in     | Path to a custom YAML behaviour template.                                                       |

### Cache and limits

| Env var                     | Default | Description                                                                                                     |
| --------------------------- | ------- | --------------------------------------------------------------------------------------------------------------- |
| `KV_POLICY_TTL_SECONDS`     | `300`   | How long to cache fetched policies (seconds).                                                                   |
| `KV_HTTP_TIMEOUT_SECONDS`   | `10`    | Per-request HTTP timeout.                                                                                       |
| `KV_TASK_MAX_AGE_SECONDS`   | `3600`  | Abandoned-task eviction threshold for `sweep_stale_tasks()`.                                                    |
| `KV_SWEEP_ENABLED`          | `true`  | Whether the background sweeper thread starts automatically. Set `false` to call `sweep_stale_tasks()` manually. |
| `KV_SWEEP_INTERVAL_SECONDS` | `300`   | How often the background sweeper runs (seconds).                                                                |
| `KV_SWEEP_FLUSH_ON_EVICT`   | `true`  | Whether to attempt a batch log post for evicted tasks before discarding their buffers.                          |

### Resilience (opt-in)

| Env var                           | Default          | Description                                                                                                                                                                            |
| --------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `KV_POLICY_FAIL_MODE`             | `open`           | `open` = allow all when no policies loaded. `closed` = block all `step_execution` behaviors when no policies are available.                                                            |
| `KV_POLICY_CACHE_PATH`            | empty (disabled) | File path for on-disk policy cache. Written after each successful fetch; loaded on cold start if the API is unreachable.                                                               |
| `KV_POLICY_CACHE_MAX_AGE_SECONDS` | `86400`          | Maximum age (seconds) of the disk cache before a staleness warning is emitted. The cache is still used when stale.                                                                     |
| `KV_POLICY_HMAC_SECRET`           | empty (disabled) | Shared secret for HMAC-SHA256 verification of policy fetch responses. Must be set on both the engine and API for signing to activate.                                                  |
| `KV_REGISTRATION_TTL`             | empty (infinite) | How long a cached agent registration is valid before re-registering. Accepts `30m`, `24h`, `7d`, or raw seconds. Empty = cache never expires (re-registration only on payload change). |

### Logging

| Env var        | Default | Description                                                                                      |
| -------------- | ------- | ------------------------------------------------------------------------------------------------ |
| `KV_LOG_LEVEL` | `INFO`  | Log level for `kyvvu` / `kyvvu_engine` Python loggers. Set to `DEBUG` for per-evaluation traces. |

***

## Policy-bundle integrity (HMAC)

`KV_POLICY_HMAC_SECRET` adds integrity and authenticity protection to the policy-distribution channel. When set, the API signs every policy-fetch response with HMAC-SHA256 (header `X-Kyvvu-Policy-Signature`, computed over the JSON body) and the engine verifies that signature before trusting the policies. It is defense-in-depth on top of TLS: TLS protects the connection, while the HMAC binds each policy bundle to a shared secret that a passive proxy or compromised intermediary cannot forge.

Set `KV_POLICY_HMAC_SECRET` to the **same value** on the API and on every verifying engine. Generate a secret with:

```bash
python3 -c "import secrets; print(secrets.token_urlsafe(48))"
```

### Rollout order

1. **Set the API first.** Engines that do not yet have the secret simply skip verification, so enabling it on the API is non-breaking.
2. **Then set the engines.** Each engine begins verifying once the secret is present on both sides.

An engine configured with a **mismatched** secret rejects every fetch and keeps serving its previously cached policies. On a cold start with no cache, an engine running `KV_POLICY_FAIL_MODE=closed` blocks all `step_execution` behaviors until the secret is aligned.

### Rotation caveat

With a single shared secret, rotation is **not** zero-downtime. During rotation the API and the engines briefly hold different secrets; fetches are rejected and policies stop updating (the last cached bundle is still enforced) until both sides are aligned again. Plan rotations for a window where a short policy-update pause is acceptable.

### Scope caveat

This is a **self-hosted / full-stack** feature: a single shared secret is only appropriate when one operator controls both the API and the engine. It is not intended for hosted / multi-tenant verification. Per-API-key / asymmetric scoping for multi-tenant deployments is tracked in issue #278.

***

## Email (password reset)

Password reset emails are sent via SMTP. In development, no SMTP configuration is needed — the reset URL is logged to the API console instead.

| Env var            | Default                 | Description                                                                 |
| ------------------ | ----------------------- | --------------------------------------------------------------------------- |
| `KV_SMTP_HOST`     | empty                   | SMTP server hostname. When empty, reset URLs are logged instead of emailed. |
| `KV_SMTP_PORT`     | `587`                   | SMTP server port (TLS).                                                     |
| `KV_SMTP_USER`     | empty                   | SMTP authentication username.                                               |
| `KV_SMTP_PASSWORD` | empty                   | SMTP authentication password.                                               |
| `KV_SMTP_FROM`     | `noreply@kyvvu.com`     | Sender email address.                                                       |
| `KV_WEB_URL`       | `http://localhost:3000` | Dashboard URL used in reset email links.                                    |

### Production setup (AWS SES)

SES is provisioned via Terraform:

```bash
cd infra/terraform
terraform apply          # creates SES domain, DKIM, SMTP user
terraform output ses_smtp_password   # copy this (sensitive)
terraform output ses_smtp_username   # copy this
```

Then set in the EC2 `.env` file:

```bash
KV_SMTP_HOST=email-smtp.eu-central-1.amazonaws.com
KV_SMTP_PORT=587
KV_SMTP_USER=<ses_smtp_username output>
KV_SMTP_PASSWORD=<ses_smtp_password output>
KV_SMTP_FROM=noreply@kyvvu.com
KV_WEB_URL=https://platform.kyvvu.com
```

Note: New SES accounts start in sandbox mode. Request production access via AWS Console → SES → Account dashboard.

***

## Next steps

* [Self-Hosted Setup](https://github.com/Kyvvu/platform/tree/main/docs/deployment/self-hosted.md) — deployment guide
* [Architecture](/core-concepts/architecture) — how components use these settings


# kv.add\_step()

**What you'll learn:** How to record individual steps into the active task without wrapping a function in `@kv.step`.

`kv.add_step()` records a step into the active task imperatively. Use it for inline steps, third-party code you cannot decorate, dynamically chosen tools, or injecting a `step.gate` (human approval) inside a callback-based agent like LangChain. It is the missing primitive for mixing integration patterns cleanly — decorator, callback, and manual — within a single agent.

***

## Method signature

```python
kv.add_step(
    step_type: str | StepType,
    step_name: str | None = None,
    *,
    input: dict | None = None,
    output: dict | None = None,
    verb: str | Verb | None = None,
    properties: dict | None = None,
    meta: dict | None = None,
    task_id: str | None = None,
) -> None
```

***

## Parameters

| Parameter    | Type                  | Default      | Description                                                                                 |
| ------------ | --------------------- | ------------ | ------------------------------------------------------------------------------------------- |
| `step_type`  | `str \| StepType`     | *(required)* | Step type enum or string (e.g. `"step.gate"`, `StepType.step_model`).                       |
| `step_name`  | `str \| None`         | `None`       | Human-readable label. Defaults to the `step_type` value.                                    |
| `input`      | `dict \| None`        | `None`       | Input data to record.                                                                       |
| `output`     | `dict \| None`        | `None`       | Output data to record.                                                                      |
| `verb`       | `str \| Verb \| None` | `None`       | HTTP-style verb (`"GET"`, `"POST"`, etc.).                                                  |
| `properties` | `dict \| None`        | `None`       | Arbitrary key-value metadata.                                                               |
| `meta`       | `dict \| None`        | `None`       | Engine-reserved multi-agent correlation fields.                                             |
| `task_id`    | `str \| None`         | `None`       | Explicit task ID. Falls back to the active task from `start_task()` or a framework adapter. |

***

## Behaviour

* Requires an active task — set by `start_task()`, `@kv.step(step_type="task.start")`, or a framework adapter such as `KyvvuLangChainHandler`.
* Evaluates policies before recording. Raises `KyvvuBlockedError` if the step is blocked by an active policy.
* Unlike `@kv.step`, no template matching is performed — the caller provides all fields explicitly.
* Unlike `@kv.step`, does **not** auto-call `error_task()` on block — the caller decides how to handle it.

***

## Usage examples

### Standalone (manual task lifecycle)

```python
from kyvvu import Kyvvu

kv = Kyvvu(api_url="...", api_key="...")
kv.register_agent(agent_key="my-agent", name="My Agent", purpose="Demo")

kv.start_task()
kv.add_step(
    step_type="step.resource",
    step_name="fetch_user_data",
    verb="GET",
    input={"url": "https://api.example.com/users/42"},
    output={"status": 200, "user": {"name": "Alice"}},
)
kv.end_task()
```

### Inside a @kv.step-decorated agent

```python
@kv.step(step_type="task.start")
def run_agent(prompt: str):
    result = call_llm(prompt)

    # Inject an inline gate — no separate function needed
    kv.add_step(
        step_type="step.gate",
        step_name="safety_check",
        input={"content": result},
        output={"result": "pass", "check_type": "content_filter"},
    )

    return result
```

### Mixed with LangChain handler

```python
from langchain.tools import tool
from kyvvu import Kyvvu
from kyvvu.integrations.langchain import KyvvuLangChainHandler

kv = Kyvvu(api_url="...", api_key="...")
kv.register_agent(agent_key="lc-agent", name="LC Agent", purpose="Demo")
handler = KyvvuLangChainHandler(kv)

@tool
def send_email(recipient: str, body: str) -> str:
    """Send an email after human approval."""
    # The handler has already started a task via on_chain_start.
    # Use add_step() to inject a gate the handler doesn't cover:
    approved = input(f"Approve sending to {recipient}? [y/n] ")
    kv.add_step(
        step_type="step.gate",
        step_name="human_approval",
        input={"action": "send_email", "recipient": recipient},
        output={"result": "pass" if approved == "y" else "fail"},
        properties={"guard": {"check_type": "human_approval"}},
    )
    if approved != "y":
        return "Blocked by human reviewer."
    return do_send(recipient, body)

# Pass the handler when invoking the chain:
agent.invoke({"input": "..."}, config={"callbacks": [handler]})
```

***

## get\_current\_task\_id()

Convenience function that returns the `task_id` active in the current context (set by `start_task()` or a framework adapter), or `None`. Useful for advanced users who need to inspect the active task without a `Kyvvu` instance reference.

```python
from kyvvu import get_current_task_id

task_id = get_current_task_id()  # str or None
```

***

## Next steps

* [Python Decorator](/integrations/decorator) — function-level step instrumentation with `@kv.step`
* [LangChain / LangGraph](/integrations/langchain) — callback-based integration
* [Atomic Behaviours](/core-concepts/behaviours) — the step types and verbs available
* [Policies and Rules](/core-concepts/policies) — how policy evaluation works


# Licensing

**What you'll learn:** The license structure across Kyvvu's packages.

***

## Package licenses

| Package         | License                               | License file           |
| --------------- | ------------------------------------- | ---------------------- |
| `kyvvu-sdk`     | Apache License 2.0                    | `kyvvu-sdk/LICENSE`    |
| `kyvvu-engine`  | Business Source License 1.1 (BSL 1.1) | `kyvvu-engine/LICENSE` |
| `kyvvu-api`     | Proprietary                           | `kyvvu-api/LICENSE.md` |
| Kyvvu Dashboard | Proprietary                           | --                     |

## SDK (Apache 2.0)

The SDK is permissively licensed. You can use, modify, and distribute it freely.

## Engine (BSL 1.1)

The engine is source-available under the Business Source License 1.1:

* **Free use** is permitted for development, testing, research, evaluation, and personal non-commercial purposes.
* **Production use** requires a Kyvvu commercial subscription or a separate license agreement with Kyvvu B.V.
* Each release **converts to Apache License 2.0** four years after its publication date.

The SDK depends on the engine at runtime. **The SDK's permissive license does not extend to the engine.** Bundling, vendoring, or depending on the SDK does not grant rights to the engine beyond what BSL 1.1 permits.

## Platform API and Dashboard (Proprietary)

The API and dashboard are proprietary software. Copyright 2025-2026 Kyvvu B.V. All rights reserved.

## Behavior definitions

The atomic behavior definitions (step types, scopes, verbs, and the Behavior schema) are part of `kyvvu-engine` and are governed by BSL 1.1.

## Pricing

Kyvvu meters usage by **active engine instance-hours**. One instance-hour is one `KyvvuRunner` (or `kyvvu serve` process) active for one hour.

|               |                                |
| ------------- | ------------------------------ |
| **Rate**      | EUR 0.05 per instance-hour     |
| **Free tier** | 1,000 instance-hours per month |

Kyvvu is currently available for evaluation at no cost — metered billing will be introduced in a future release. While in beta, we reserve the right to cap user registration or limit policy retrieval and agent registration.

## Commercial licensing

For production use of the engine, commercial subscriptions, or licensing inquiries:

**<licensing@kyvvu.com>**

***

## Next steps

* [Installation](/getting-started/installation) — install the SDK
* [Changelog](/reference/changelog) — version history


# Changelog

**Version history for the Kyvvu platform.**

For full release notes and older versions, see [GitHub Releases](https://github.com/Kyvvu/platform/releases).

***

## v0.15.0 — Claude hardening & dashboard scale (2026-06-19)

Engine 0.7.0 · SDK 0.15.0 · kyvvu-claude 0.2.0 · API 0.12.0 · Web 0.9.0.

### New: dashboard tasks-list endpoint & server-paginated trace view (#240)

* `GET /api/v1/logs/tasks` — a distinct-task summary endpoint. A SQL group-by on `task_id` returns one row per task (`task_id`, `agent_id`, `agent_name`, `environment`, `step_count`, `first_ts`, `last_ts`, `status`) with offset-based pagination (`skip` / `limit`, default 20, max 100) and a total count of distinct tasks. Status is derived in SQL via conditional aggregation over `task.end` / `task.error` steps (error > completed > running), portable across PostgreSQL and SQLite. Ownership/auth mirror `GET /logs`; the `/tasks` route is registered before `/{log_id}`. Adds `TaskSummary` / `TaskListResponse` schemas.
* Dashboard trace view now lists tasks server-side (20/page) and lazy-loads a task's steps via `GET /logs?task_id` only when a row is expanded. Replaces the previous flat 1000-log fetch + client-side grouping, which never showed more than \~2 tasks for 500+-step agents.

### Changed: shared template engine & path-containment (#239)

* The `BehaviorTemplate` loader / matcher / interpolation machinery moved into `kyvvu_engine.templates` (canonical, integration-agnostic). The name registry (`from_builtin` / `_BUILTIN_TEMPLATES`) was removed — integrations load their template by path. Per-integration `*.template.yaml` files stay with their integration.
* kyvvu-claude mapper is now template-driven (`claude-code.template.yaml`, all 29 named tools + MCP catch-all) instead of a hardcoded if-tool ladder. A Claude-side pre-pass derives a Bash write target (`>`, `>>`, `tee`, `cp`, `mv`, `dd of=`, `sed -i`) → `target.host`, and detects shell secret reads (`cat`/`cp`/… of `.env`/`.pem`/…) → `data.classification:secret`, so the scope-containment and tainted-path policies arm for Bash. Shell parsing stays in kyvvu-claude; the engine never learns shell syntax.
* New `path_within_root` rule (allowlist containment): returns true when the target resolves at/under `EvalContext.project_root`, false when it escapes. Scoped to writes (POST/PATCH/DELETE/exec), fails open when `project_root` is unset. The Claude-code-safety "No write outside project" policy now uses it instead of a hardcoded denylist. Paths are normalized lexically (`..` collapsed; symlinks not resolved — documented limit).
* kyvvu-claude `PreToolUse` hook populates `EvalContext.project_root` from the session cwd.

### Changed: claude-code-safety destructive-command policies (#238)

* The single mis-scoped "No rm -rf root" policy (which critical-blocked scoped cleanups like `rm -rf /tmp/x` while `rm -fr /`, `rm -r -f /`, and parent-traversal chains slipped through) was replaced with four per-target, force-agnostic, anchored policies: root, home, parent traversal, and system directories (`/usr`, `/var` intentionally excluded). Recursive detection tolerates flag order/splits.
* All six `exec.command` destructive policies (rm + git) gained a command-position prefix (leading separator `;`/`&&`/`||`/`|` or exec-wrapper `sudo`/`doas`/`env`/`xargs`/`time`/`nice`) so prefixed and chained dangerous commands (`sudo rm -rf /`, `cd x && rm -rf /`, `build; git reset --hard`) are caught while `echo rm -rf /` still passes (the engine matches `field_matches_regex` start-anchored).
* New stopgap shell policies: no shell write to sensitive paths (`redirect`/`tee`/`cp`/`mv`/`dd`/`install` into `/etc` `/usr` `/var` `~/.ssh` `~/.aws` …) and no shell read of secret files (`cat`/`less`/`head`/`base64`/… of `.env`/`.pem`/`.key`/`credentials`/…), mirroring the destructive-delete pattern directly on `exec.command`.
* Match/no-match + known-gap fixtures (`tests/rm_policies.py`, `tests/shell_policies.py`) load the live manifest regexes and exercise them with the engine's `re.match` / `DOTALL` semantics.

### Changed: kyvvu-claude sub-agent partial-order stamping & session concurrency (#237)

* Transcript cursor is now per-path (`transcript_cursors: dict[path -> lines_read]`) instead of a single int, so the main transcript and each sub-agent transcript (`…/subagents/agent-{id}.jsonl`) advance independently — fixes skipped / double-counted `step.model` records. Legacy scalar cursors migrate on load.
* New `mutate_session()` / `transact_session()` context manager holds the file lock across the full read-modify-write, so concurrent hook processes no longer lose a step/history entry. The post-threshold flush trims by count (not `history.clear()`) so concurrent appends survive.
* New `subagent.py`: every step is stamped with `properties["kyvvu.subagent"] = {id, depth, spawn_step}` (flat storage). Tag derives from `transcript_path` (`main` for the parent, `agent-{id}` for a sub-agent); within-sub-agent order reconstructs by `(tag, timestamp)`; the parent→child `spawn_step` back-reference comes from a new `SubagentStart` lifecycle hook. Cross-sibling order is deliberately dropped. Tags are storage-only and never scope enforcement: replay stays whole-task, so a secret read in one sub-agent still taints exec anywhere in the task.
* kyvvu-claude CLI `init` / `status` now surface the configurable incident sink (`KV_INCIDENT_LOCATION` / `KV_INCIDENT_FORMAT`, #225).

***

## v0.14.x — Standardized logging, integrations & hardening (June 2026)

Sprint 1-3. Engine 0.6.x, SDK 0.14.x, kyvvu-claude 0.1.x.

### New: kyvvu-claude (Claude Code integration)

* Hook-based Claude Code integration — policy enforcement via PreToolUse / PostToolUse / SessionStart / SessionEnd hooks.
* Claude Code tools mapped to Kyvvu behaviors (Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, Agent, MCP tools, …).
* LLM call capture from the session transcript (model name, token usage, proposed tools).
* Secret file classification: `.env`, `.pem`, `.key`, etc. tagged with `data.classification:secret` for tainted-path policies.
* Incident reporting to the API on policy blocks.
* Hooks **deny** the offending tool call but do not stop the session; a tainted path is permanent for the session.
* Published to PyPI as `kyvvu-claude` (Apache 2.0).

### New: Claude Code Safety manifest

* 9 policies (all severity `critical`): credential-exfiltration guard (no exec / no network after secret read), destructive-command protection (force push, `rm -rf /`, `git reset --hard`), scope containment (no write outside project), runaway prevention, and PII scanning in commands.
* Tainted-path policies key on `data.classification:secret`.

### New: Standardized logging & exporters (#212)

* Pluggable exporters: stdout, file (JSONL), Kyvvu HTTP API, OTLP, callback.
* Policy evaluation results embedded in the audit trail — `meta["kyvvu.eval"]` on every step (action, risk\_score, per-policy outcomes).
* OTLP shipped as a default engine dependency.

### New: LangGraph & CrewAI integrations (#213, #214, #218)

* `KyvvuLangGraphHandler` — node metadata (`langgraph.node_name`), GraphInterrupt mapped to `step.gate`, tool extraction from graph nodes. LangGraph Research example agent (port 8105).
* `KyvvuCrewAIListener` — event-bus integration, parallel tool-call pairing via `event_id` / `started_event_id`, `step.message` framing, and a circuit breaker (`listener.is_blocked`) for post-kickoff block detection. CrewAI Research example agent (port 8106).

### Changed: canonical `log_location` / `log_format` (#222)

* `log_location` (where) and `log_format` (how) are now the single canonical logging settings across engine, SDK, and kyvvu-claude. Resolvers: `effective_log_location` / `effective_log_format`.
* Legacy `log_endpoint` / `log_exporters` settings removed entirely.

### Changed: configurable incident sink (#225)

* `incident_location` / `incident_format` configure where incidents are sent and in what format. Both default to the trace sink (the incident location is derived from the log location; the Kyvvu HTTP path is `…/api/v1/incidents`).

### Changed: blocked steps in the audit trail (#219)

* Blocked steps are now always recorded with `output.status=blocked` (previously dropped). `task.error` + `end_task` fire on block so the audit trail is always flushed. `KyvvuBlockedError` propagates correctly across all integrations (decorator, LangChain, LangGraph, CrewAI, kyvvu-claude).

### New: manifest provenance in the audit trail (#217)

* Policies materialised from a manifest assignment now carry their origin. The policy-fetch response includes `manifest_id`, `manifest_name`, `manifest_repo`, `manifest_path`, `manifest_sha`, `manifest_assigned_at`, and `manifest_assigned_by` (null for hand-authored policies).
* Engine: `EvalContext.manifests` (list of `ManifestRef`), optional `manifest_id` / `manifest_name` on each loaded policy, `manifest_id` on each per-policy result. The in-effect manifest set is attached to `meta["kyvvu.manifests"]` on the `task.start` atom, and per-policy `manifest_id` appears in `meta["kyvvu.eval"]["policies"][i]`.
* No database migration: the `Assignment` model already stored these fields.
* Provenance is now also surfaced on the single-policy `GET` and standard policy-list responses (round-tripped via `PolicyResponse`), not just the policy-fetch endpoint (#217 follow-up).

### Hardening & code quality (#233, #164)

* SDK schemas ported to `(str, Enum)` enums and Pydantic v2 payloads with a byte-stable serializer.
* Regex parity: `field_matches_regex` uses `re.DOTALL` on both the pre-compiled (PolicyStore) and fallback paths, so compiled and uncompiled evaluation behave identically.
* API: full strict-mypy compliance across all modules (type annotations only, no runtime changes).

### Infrastructure

* `deploy-main.yml`: inline PyPI publish (no cross-workflow trigger dependency) and build-before-down (downtime \~2 min → \~5 s).
* kyvvu-claude test job added to `merge-dev.yml` and `deploy-main.yml`.
* `KV_LOG_LEVEL` passed to the API container in production.
* Nginx routes for the LangGraph (8105) and CrewAI (8106) demo agents.
* `bump-version.sh` includes kyvvu-claude.

***

## v0.13.0 — Manifests (2026-05-14)

### Platform

* **Manifests**: YAML policy files in GitHub repos replace server-side policy templates
* **Assignments**: Assign manifests to agents with per-agent policy materialization and risk filtering
* **Repos**: Connect GitHub repos (public or private) with encrypted token storage
* **Dashboard restyle**: Sidebar navigation (6 tabs), Settings page with sub-tabs (API Keys, Platform Events, Reports, Organization, Members)
* **Dashboard overview**: 4 stat cards, recent incidents table, recent activity feed
* **Agents page**: simplified 5-column table, last policy fetch, expandable row with assigned manifests, view policies
* **Repo delete protection**: 409 if active assignments exist
* **Policy unique constraint**: includes agent\_id to allow same manifest on multiple agents
* **Hash chain validation**: re-enabled in dashboard via `verify_either_auth`

### CLI

* `kyvvu list-manifests` — list available manifests from connected repos
* `kyvvu assign-manifest` — assign a manifest to an agent
* `kyvvu list-assignments` — list manifest assignments
* `kyvvu init` simplified — pure scaffolding, no auth required, no policy application
* `kyvvu list-policies` — new Source column showing manifest origin

### Removed

* Policy template endpoints (`/api/v1/policy-templates/*`)
* Templates and Policies dashboard tabs
* Default seeded policies (replaced by manifest assignment)
* `TemplateLoader`, template YAML files, template router, 97 template tests

***

## v0.11.0 (2026-04-29)

### Engine

* 26 built-in rule functions across six categories (field, path, count, classification, content, flow).
* Compound rules: `all_of`, `any_of`, `not` with unlimited nesting.
* `KyvvuRunner` with TTL-based policy fetch, incident webhooks, and log flush.
* `kyvvu serve` — local HTTP server for non-Python agents.
* Performance: p99 < 300 us with 100 policies and 50-step history.
* `explain()` method for human-readable evaluation traces.
* `sweep_stale_tasks()` for abandoned task cleanup.
* Payload redaction (`KV_LOG_PAYLOADS=metadata_only`).

### SDK

* `@kv.step` decorator with sync and async support.
* `KyvvuLangChainHandler` for LangChain and LangGraph.
* YAML behaviour templates with first-match-wins evaluation and deep-merge.
* `FrameworkAdapter` base class for custom integrations.
* Programmatic task API: `start_task()`, `end_task()`, `error_task()`.
* `ContextVar`-based concurrent task tracking.
* CLI: `kyvvu register`, `login`, `logout`, `whoami`, `init`, `serve`, `list-agents`, `list-policies`.

### API

* Policy templates with YAML definitions and apply/remove API.
* OWASP Top 10 for Agentic Applications default template (8 policies).
* Organization-scoped data isolation.
* Incident lifecycle: open / active / resolved / ignored.
* Hash chain for tamper-evident log ingestion.
* Registration cap with waitlisting.
* `declared_tools` field on agent registration.
* Hosted engine evaluation endpoint (`POST /api/v1/engine/evaluate`).
* Audit report generation (PDF, XML).

### Dashboard

* Policy management with dynamic forms from rule schemas.
* Template application UI.
* Incident triage workflow.
* Log viewer with hash chain validation.
* Organization member management.

***

## Next steps

* [Licensing](/reference/licensing) — license structure
* [Installation](/getting-started/installation) — get started


