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

# 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:
    # (open the dashboard, go to Workspace → API Keys, and create one)
    cp .env.example .env       # add your KV_API_KEY
    # or: export KV_API_KEY=your-key

    # Run the agent:
    python agent.py

Your agent is governed from its first step: Kyvvu applies its
baseline (15 policies, OWASP + EU AI Act) when the agent registers.
No assignment needed — see them on the Agents page, tagged Baseline.

To add more 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>
```

## Get an API key

The agent needs a `KV_API_KEY` to register. Create one in the dashboard under **Workspace → API Keys**.

`kyvvu auth` logs the *CLI* in and deliberately cannot mint an agent key — a CLI login must never be able to escalate itself into an agent-scoped credential. See [Installation](/getting-started/installation.md) for the full explanation.

## Run the demo agent

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

That is the whole first run. There is **no assignment step before it** — see [Your agent is governed from its first step](#your-agent-is-governed-from-its-first-step) below — and there could not be one even if you wanted it: the agent does not exist until `register_agent()` runs, so there is no agent id to assign a manifest to until after this command.

The full `agent.py` source:

```python
"""Minimal Kyvvu agent -- demonstrates task and step instrumentation.

This agent uses mocked LLM and resource calls so it runs offline with
no extra API keys. The point is to show Kyvvu's trace output -- every
step you see in the terminal is a behavior the policy engine evaluates.

Run:
    export KV_API_KEY=KvKey-...   # dashboard -> Workspace -> API Keys
    python agent.py
"""
from __future__ import annotations

import os
import sys

from kyvvu import Kyvvu, KyvvuBlockedError, RiskClassification, StepType, Verb


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,
)

kv.register_agent(
    name="Hello Kyvvu",
    purpose=(
        "Demonstration agent for getting started with Kyvvu "
        "\u2014 shows Kyvvu's behavioral trace and policy "
        "evaluation with mocked LLM and resource calls."
    ),
    declared_tools=["call_llm", "fetch_user_data", "run_script"],
)


@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.

    The "Code execution requires a preceding gate" policy blocks step.exec
    unless the task history already holds a step.gate whose
    properties["guard"]["result"] is "pass". *Anywhere* earlier in the task
    counts -- check_step_requires_gate scans the whole history, and a bare
    step.gate with no guard result does not clear the block.

    Immediate precedence belongs to a different bundled policy, "External
    content taint: high-impact actions require a fresh gate", which uses the
    step_directly_preceded_by rule. The distinction is the point of policies
    on paths: "approved at some point in this task" and "approved after the
    untrusted data arrived" are different guarantees.

    This demonstrates Kyvvu's runtime enforcement of OWASP ASI05.
    """
    return f"(would execute: {code!r})"


def handle_request(user_id: str, query: str) -> str:
    user = fetch_user_data(user_id)
    answer = call_llm(f"User {user['name']} asks: {query}")
    # This will be blocked by the OWASP policy -- no gate precedes it:
    run_script(f"save_response('{answer}')")
    # Reached only when nothing blocked the step above. On a governed agent
    # this line never runs, which is the whole point of the demo -- but the
    # function still has to keep the promise its `-> str` annotation makes.
    return answer


if __name__ == "__main__":
    try:
        kv.start_task()
        result = handle_request(user_id="user_123", query="What's the weather?")
        print(f"\nResult: {result}")
    except KyvvuBlockedError as exc:
        print(f"\n\u26d4 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"   \u2022 {p.name} ({p.severity})")
        print(
            "\nThis is Kyvvu working as intended. In a real agent, you'd\n"
            "record an approval gate before the blocked action -- and it only\n"
            "counts when it carries a result:\n"
            "  kv.add_step(StepType.step_gate, \"human_approval\",\n"
            "              properties={\"guard\": {\"result\": \"pass\"}})"
        )
        try:
            kv.error_task(error=exc)
        except Exception:
            pass  # Task may not have started (blocked on task.start)
        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, runs three steps, and the third one is blocked:

```
kyvvu.core INFO Agent registered: agent_id=agent_0d0aaa3d23cf233c
kyvvu_engine.io.runner INFO Policies loaded: 15 policies for agent_key='my-agent'
kyvvu_engine.io.runner WARNING Agent registration policy violation: action=warn risk=0.75
kyvvu_engine.io.runner INFO policy violation: task=d4c09846-8aa step='call_llm' action=warn risk=0.75 (1 violations)
kyvvu_engine.io.runner INFO policy violation: task=d4c09846-8aa step='run_script' action=block risk=1.00 (1 violations)
kyvvu_engine.io.runner INFO task d4c09846-8aa ended — 4 steps (task.start×1, step.resource×1, step.model×1, task.error×1)
kyvvu_engine.io.exporters.http INFO HttpExporter: batch accepted task=d4c09846-… steps=4 hash_tail=939581eb734cb598…

⛔ Policy blocked this step: Step 'run_script' blocked (risk_score=1.00): Code execution requires a preceding gate
   Risk score: 1.00
   Action:     block
   • Code execution requires a preceding gate (critical)

This is Kyvvu working as intended. In a real agent, you'd
record an approval gate before the blocked action -- and it only
counts when it carries a result:
  kv.add_step(StepType.step_gate, "human_approval",
              properties={"guard": {"result": "pass"}})
```

`HttpExporter: batch accepted` is the line that matters for onboarding: the task's steps reached the platform, so they are on the **Logs** page now. You did not configure that. `log_location` defaults to `auto`, which follows the policy source — an agent whose policies come from the platform sends its trace there too. Set `KV_LOG_LOCATION=stdout` if you would rather keep a run local.

## Your agent is governed from its first step

Registering an agent applies Kyvvu's **baseline** to it automatically — 15 policies from two bundled manifests, the OWASP Top 10 for Agentic Applications default security template and EU AI Act minimal/limited-risk compliance. There is no manual assignment step, no repository to connect, and nothing to configure. The dashboard shows them on the agent's row, tagged **Baseline**, with the version of each bundled manifest.

The SDK ships the same 15 policies offline, so an agent that cannot reach the platform on its first run is still governed by them rather than by nothing. A successful policy fetch replaces that set with the platform's answer.

The baseline is **on by default, not mandatory**: you can unassign either manifest from the dashboard, and registering again will not put it back.

### Why the demo agent shows a compliance warning

With the default `store_metadata=False`, the platform stores no name, purpose or owner for your agent — so the baseline's EU AI Act documentation policies have nothing to check and report a **warning** (not a block; the agent runs normally). That is the honest answer: Kyvvu is reporting what it actually holds about the agent, and a governance tool that said "compliant" about data it does not have would be worse than one that says so.

Pass `store_metadata=True` to record the agent's documentation on the platform. `owner_id` is then filled in automatically from the email of the API key's owner if you do not supply one, since that is the accountable party (EU AI Act Article 16).

### Your first run also opens incidents

Every policy violation the engine reports is sent to the platform as an **incident**, so the Incidents page is not empty after one `python agent.py` either. This is deliberate — a governance product whose violations reach only a terminal is the same failure as one whose traces do — but it is worth knowing what you are looking at. A first run of the unmodified demo opens **seven**:

| incidents                                             | why                                                                       | do they pile up?                                                            |
| ----------------------------------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| 5 × EU AI Act / OWASP documentation policies          | artifacts of `store_metadata=False`, exactly the warnings explained above | no — `agent_registration` incidents upsert per (agent, policy, environment) |
| "Code execution requires a preceding gate"            | the demo's punchline                                                      | **yes** — `step_execution` incidents are one per task run, by design        |
| "AI-generated content must be marked before delivery" | the model call is not followed by a disclosure step                       | **yes**, same reason                                                        |

To make the five documentation incidents stop, pass `store_metadata=True` so the platform holds the fields those policies check. To stop the other two, give the demo a passing gate and a disclosure step — or resolve them, which is what the Incidents page is for.

Whether a registration-metadata violation should become an incident at all when you have deliberately chosen not to store that metadata is an open product question, tracked as [#456](https://github.com/Kyvvu/platform/issues/456).

Incidents follow the trace sink: `KV_INCIDENT_LOCATION=none` turns them off without touching the trace, and `KV_INCIDENT_LOCATION` takes the same vocabulary as `KV_LOG_LOCATION` (including `auto`).

{% 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 %}

## 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" searched the task history for a `step.gate`.
* None was found. The policy has severity `critical`, so the engine returned `block`.
* The decorator raised `KyvvuBlockedError` instead of executing the function.

### Clearing the block

A bare `step.gate` is **not** enough. `step_requires_gate` matches on the properties **recorded on the gate step** — specifically `properties.guard.result` against the policy's `gate_result` parameter, which defaults to `"pass"`. A gate recorded with no properties, or with `"approved"`, leaves the block exactly where it was:

| properties recorded on the gate step                        | verdict        |
| ----------------------------------------------------------- | -------------- |
| none                                                        | `block` (1.00) |
| `step.gate` with no `properties`                            | `block` (1.00) |
| `step.gate`, `properties={"guard": {}}`                     | `block` (1.00) |
| `step.gate`, `properties={"guard": {"result": "approved"}}` | `block` (1.00) |
| `step.gate`, `properties={"guard": {"result": "pass"}}`     | `allow` (0.00) |

`kv.add_step()` records exactly the properties you hand it, so that table is what you get. The working remedy — the one the demo prints — records the result:

```python
kv.add_step(
    StepType.step_gate,
    "human_approval",
    properties={"guard": {"result": "pass"}},
)
run_script("save_response('...')")   # now allowed
```

See [`kv.add_step()`](/reference/add-step.md).

{% hint style="danger" %}
**A decorated gate does not record your function's decision — and a denial still clears the block.**

`@kv.step(step_type="step.gate")` does not read the wrapped function's return value. The decorator builds the step from `decorator.template.yaml`, whose gate rule synthesises `guard: {check_type: "approval", result: "pass"}` on *every* decorated gate. Neither the return value nor a `result=` keyword argument changes it. Measured against the bundled baseline:

| gate recorded by the decorator                                 | verdict        |
| -------------------------------------------------------------- | -------------- |
| no gate at all                                                 | `block` (1.00) |
| `@kv.step(StepType.step_gate)` whose function returns a denial | `allow` (0.00) |

So a "human approval" function that refuses still satisfies a `critical` OWASP policy, and the audit trail records a passing gate nobody granted. That is an enforcement defect, tracked as [#455](https://github.com/Kyvvu/platform/issues/455).

**Until it is fixed, record gates with `kv.add_step()`**, as above — that form carries the decision you actually made.
{% endhint %}

### "Preceding", not "immediately preceding"

`step_requires_gate` scans the **whole task history**. A passing gate recorded at the start of the task still satisfies it at the end, with any number of other steps in between.

Immediate precedence is a different bundled policy: *"External content taint: high-impact actions require a fresh gate"*, built on the `step_directly_preceded_by` rule. Once a `step.resource GET` has pulled in externally-trusted content, that policy requires the gate to be the step immediately before the high-impact action — an approval given before the untrusted data arrived does not count.

The two answer different questions: *"was this approved at some point?"* and *"was this approved after we saw the untrusted input?"* Both ship in the baseline, and both are what "policies on paths" means.

## Inspect the policies

```bash
kyvvu list-policies
```

Because of the baseline, this is **not** empty on a fresh agent — 15 policies, before you assign anything:

```
 NAME                                                        ENFORCEMENT POINT   RULE_TYPE                  SEVERITY   ENABLED
 Agent must declare a substantive purpose                    agent_registration  field_matches_regex        high       ✓
 Agent must declare a tool allowlist                         agent_registration  field_not_empty            high       ✓
 Agent must declare a valid risk classification              agent_registration  field_in_list              critical   ✓
 Tool calls must be in the declared allowlist                step_execution      step_name_in_allowlist     critical   ✓
 Code execution requires a preceding gate                    step_execution      step_requires_gate         critical   ✓
 Destructive resource operations require a preceding gate    step_execution      step_requires_gate         critical   ✓
 External content taint: high-impact actions require a …     step_execution      not                        critical   ✓
 Bound resource calls per task to prevent runaway loops      step_execution      execution_max_steps        high       ✓
 Agent must have a documented purpose                        agent_registration  field_not_empty            high       ✓
 Agent must have an identifying name                         agent_registration  field_not_empty            high       ✓
 Risk classification must be valid                           agent_registration  field_in_list              critical   ✓
 Agent must have a designated owner                          agent_registration  field_not_empty            medium     ✓
 Agent purpose must be substantive                           agent_registration  field_matches_regex        medium     ✓
 AI-generated content must be marked before delivery         step_execution      step_directly_preceded_by  high       ✓
 Chatbot disclosure gate required before outbound messages   step_execution      step_requires_gate         high       ✓
```

(A `SOURCE` column, omitted here for width, names the bundled manifest each policy came from.)

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 operations require a preceding gate" with different parameters (`target_step_types: ["step.resource"]`, `target_verb: "DELETE"`).

For the full details of each policy, see [The Bundled Baseline](/policy-authoring/bundled-baseline.md) and [OWASP Default Manifest](/policy-authoring/owasp-default.md).

## Add more policies

This step is **optional**, and it comes after the first run — your agent has an id only once it has registered, and `kyvvu assign-manifest` needs one.

The baseline is a floor to build on, not the whole story. Additional policies are managed via **manifests** — YAML files stored in a GitHub repository:

1. Go to the **Manifests** page in the dashboard
2. `Kyvvu/manifests` is **already connected** — every new workspace starts with the public library, so the page is never empty and there is nothing to set up. Connect your own repository as well if you keep manifests in one.
3. Click **Assign** on a manifest and select your agent

Or use the CLI — `kyvvu list-agents` prints the id the platform assigned:

```bash
kyvvu list-agents
kyvvu list-manifests
kyvvu assign-manifest --agent-id <id> --repo-id <id> --manifest manifests/security/data-exfiltration-guard.yaml
```

{% hint style="warning" %}
**Manifests are alternatives, not layers — and the baseline already occupies 15 policy names.** Assigning a manifest that shares a policy name with something your agent already has returns a 409 naming the overlap.

The two manifests Kyvvu applies automatically, `owasp-agentic-default.yaml` and `eu-ai-act-minimal.yaml`, are the same files published in `Kyvvu/manifests` — so assigning either of those from a repository always conflicts. Your agent already enforces them; there is nothing to do. `eu-ai-act-high-risk.yaml` and `internal-ai-starter.yaml` overlap partially for the same reason.

`data-exfiltration-guard.yaml`, `gdpr-data-subject-rights.yaml`, `healthcare-nen7510-hipaa.yaml` and `financial-services-dora-mifid.yaml` share no names with the baseline and layer on top of it cleanly.

To manage a baseline manifest from your own repository instead — to edit it, for instance — unassign the matching baseline manifest from the agent first.
{% endhint %}

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

***

## Next steps

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