> 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/policy-authoring/bundled-baseline.md).

# The Bundled Baseline

**What you'll learn:** What the 15-policy baseline set bundled inside the `kyvvu` package is, how to load it with no account and no network, why it is frozen, and where its licence boundary sits.

***

## Overview

The `kyvvu` package ships a **frozen baseline policy set**: 15 policies that make an agent governed without an account, an API key, or a network round trip.

| Manifest                | Policies | What it contributes                                                                                                                          |
| ----------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `owasp-agentic-default` | 8        | OWASP Top 10 for Agentic Applications (2026). The set's only source of `critical` step policies — four of them, and therefore the **block**. |
| `eu-ai-act-minimal`     | 7        | EU AI Act Article 50 transparency plus registration-time documentation. Both step policies are `high`, so they **warn**.                     |

Eight of the 15 are evaluated at `agent_registration` and seven at `step_execution`. There are no name collisions between the two manifests, so both can also be assigned to the same agent on the platform.

## Loading it

The fastest route is the CLI, which needs no code at all:

```bash
pip install kyvvu
kyvvu try                       # the bundled set against a demo agent path
kyvvu serve --baseline          # the same set over HTTP, still offline
```

In code:

```python
from kyvvu.baseline import load_baseline_policies
from kyvvu_engine import PolicyEngine

engine = PolicyEngine()
engine.load_policies(load_baseline_policies())

report = engine.load_report()
print(report.state.value, report.offered, report.enforcing)
assert (report.offered, report.enforcing) == (15, 15)
```

`load_baseline_policies()` reads only the two YAML files bundled inside the package, via `importlib.resources` — so it works from an installed wheel, in a container, and on an airgapped machine. It takes no arguments, makes no HTTP request, and reads no environment variable. Each call returns an independent deep copy, so mutating the result cannot affect the next caller.

Always check `load_report()` rather than `policy_count()`: a count of zero cannot tell "nothing assigned" from "every policy was rejected", and both allow everything.

## Declare the agent's tools, or every tool call blocks

Evaluation needs an `EvalContext`, and one field of it is not optional in practice. The OWASP allowlist policy reads **`agent_allowed_tools` from the context** — not `declared_tools` from the agent payload — and it **fails closed**: with the allowlist absent or empty, every `step.resource`, `step.exec` and `step.credential` is blocked with *"Tool calls must be in the declared allowlist"*.

That is the correct default for a governance product, and it is also the single easiest way to get a confusing first run: a minimal `EvalContext(agent_id=..., task_id=...)` blocks step one and attributes step three's block to the allowlist rather than to OWASP ASI05. So declare the tools:

```python
from kyvvu_engine.schemas import Action, Behavior, EvalContext, StepType, Verb

context = EvalContext(
    agent_id="hello-kyvvu",
    task_id="task-1",
    risk_classification="minimal",
    agent_allowed_tools=["fetch_user_data", "call_llm", "run_script"],
)
```

When you drive the engine through the SDK (`Kyvvu` + `@kv.step` or a framework handler), this is filled in from `Kyvvu(declared_tools=[...])`, `KV_DECLARED_TOOLS`, or an explicit `register_agent(declared_tools=[...])`. Declare it on `EvalContext` yourself when calling `PolicyEngine` directly.

## What it does to a three-step agent

The agent scaffolded by `kyvvu init` calls `fetch_user_data`, then `call_llm`, then `run_script` with no gate in front of it:

| Step                                    | Action  | Risk score | Policy                                                                   |
| --------------------------------------- | ------- | ---------- | ------------------------------------------------------------------------ |
| `fetch_user_data` (`step.resource` GET) | `allow` | 0.00       | —                                                                        |
| `call_llm` (`step.model`)               | `warn`  | 0.75       | AI-generated content must be marked before delivery (Art. 50(3), `high`) |
| `run_script` (`step.exec`)              | `block` | 1.00       | Code execution requires a preceding gate (OWASP ASI05, `critical`)       |

Continuing from the snippet above — a step is only part of task history once it is **recorded**, and a blocked step is never recorded:

```python
steps = [
    ("fetch_user_data", StepType.step_resource, Verb.GET),
    ("call_llm", StepType.step_model, None),
    ("run_script", StepType.step_exec, None),
]

decisions = []
for step_name, step_type, verb in steps:
    behavior = Behavior(
        agent_id="hello-kyvvu",
        task_id="task-1",
        step_type=step_type,
        verb=verb,
        step_name=step_name,
        input={"query": "What's the weather?"},
    )
    result = engine.evaluate(behavior, context)
    violated = [policy.name for policy in result.policies if policy.violated]
    decisions.append((step_name, result.action.value, result.risk_score, violated))
    if result.action != Action.block:
        engine.record(behavior)

assert decisions == [
    ("fetch_user_data", "allow", 0.00, []),
    ("call_llm", "warn", 0.75, ["AI-generated content must be marked before delivery"]),
    ("run_script", "block", 1.00, ["Code execution requires a preceding gate"]),
]
```

Those three `assert`s are not decoration: `kyvvu-sdk/tests/test_baseline.py` executes every Python block on this page in order, so if the behaviour ever stops matching the table above, this page fails CI rather than quietly misleading a reader.

That is why both manifests ship rather than one: the OWASP set owns the block, and the EU AI Act set layers a citable warn on top without stealing it. Only `critical` reaches weight 1.0 and blocks; `high`, `medium` and `low` warn. An EU-only set contains no `critical` step policy, so it produces no block at all on a typical agent.

## Clearing the block: a gate must record a *result*

The block is path-dependent, not a ban on `step.exec`. But "add a gate" is not enough. `step_requires_gate` looks for a `step.gate` in task history whose **`properties.guard.result` is exactly `"pass"`**:

| Gate recorded before `run_script`         | Verdict      |
| ----------------------------------------- | ------------ |
| none                                      | `block` 1.00 |
| `step.gate` with no properties            | `block` 1.00 |
| `guard.check_type` set, no `guard.result` | `block` 1.00 |
| `guard.result: "approved"`                | `block` 1.00 |
| `guard.result: "pass"`                    | `allow` 0.00 |

```python
gate = Behavior(
    agent_id="hello-kyvvu",
    task_id="task-1",
    step_type=StepType.step_gate,
    step_name="human_approval",
    properties={"guard": {"check_type": "human_approval", "result": "pass"}},
)
engine.record(gate)
```

Through the SDK, the same thing is one call: `kv.add_step(StepType.step_gate, "human_approval", properties={"guard": {"check_type": "human_approval", "result": "pass"}})`.

## Two rough edges to know about

Both are in the manifests rather than in this loader, both are tracked upstream in [#234](https://github.com/Kyvvu/platform/issues/234), and neither is papered over by the tests:

* **The registration allowlist check is looser than the step-level one.** "Agent must declare a tool allowlist" uses `field_not_empty`, which rejects only `None` and `""` — so `[]`, `{}`, `0` and `False` all satisfy it. The step-level policy then fails closed on that same empty allowlist. The net effect is over-blocking rather than fail-open, which is the right direction, but it is a confusing pair: the set reports the registration policy satisfied for an agent it will block on every tool call.
* **The scaffolded demo agent warns at registration.** "Agent must have a designated owner" reads `owner_id`, which the `kyvvu init` scaffold does not pass and the platform does not default, so the registration verdict for the scaffold as written is `warn` 0.50 rather than `allow`. Worth knowing before the baseline is applied automatically at registration.

## Provenance

`baseline_manifests()` is the assert-able answer to "where did this policy come from?":

```python
from kyvvu.baseline import baseline_manifests

for manifest in baseline_manifests():
    print(manifest.id.value, manifest.version, manifest.policy_count, manifest.license)
    print(manifest.source_path)
    print(manifest.policy_names)
```

Every policy dict also carries `manifest_name`, so a caller reading a policy — or rendering a violation — can name the manifest it came from without re-reading the YAML. `manifest_id` is deliberately left unset: it means "`Assignment` row id" on the platform, and a bundled policy has no assignment behind it.

The bundled files are byte-identical copies of `kyvvu-manifests-public/manifests/security/owasp-agentic-default.yaml` and `.../compliance/eu-ai-act-minimal.yaml`, and a test enforces that, so the bundle cannot drift from the source of truth.

## Frozen, and bundled rather than fetched

**Frozen** means there is no supported knob: no flag, no environment variable, and no per-policy enable/disable. The set is the set. This is a product boundary rather than a security boundary — a local process can always reach past it, and no effort is spent pretending otherwise. For the same reason the bundle carries no integrity check: `KV_POLICY_HMAC_SECRET` protects *fetched* policies on the authenticated path, where a third party sits between the source and the engine.

**Bundled** rather than fetched because fetching from GitHub or a Kyvvu endpoint would make "no account, no network" false, put a first run behind someone else's uptime, and inherit the unauthenticated GitHub rate limit. Bundling is version-pinned and reproducible: an evaluator's demo cannot break because a manifest was edited upstream.

Freshness lives where it matters. The authenticated path already fetches current policies from the platform, which is also where assignments, incidents and the audit trail live. **Bundle = free and frozen; fetch = authenticated and current.**

## Removing and restoring the baseline

On the platform the baseline is **on by default, not mandatory**. Unassigning a baseline manifest from an agent (the Agents page, or `kyvvu unassign-manifest --assignment-id <id>`) disables its policies, and the platform treats that as a decision. The SDK registers the agent on every process start, so registering again never re-applies a manifest you removed.

Putting it back is a separate, deliberate act:

* **Dashboard.** Open **Manifests**. The bundled baseline is listed there as **Kyvvu baseline**, next to your connected repositories, and each manifest has an **Assign** button. The agent's row on the **Agents** page also still shows the baseline after you have removed all of it, so you can see what was removed.
* **API.** `POST /api/v1/assignments` with the agent's `agent_id` and the bundled manifest's `manifest_path`, and **no** `repo_id`. `GET /api/v1/baseline` lists the paths that are accepted; any other path without a `repo_id` is rejected with `422`. It needs the same permissions as any other assignment.

The restored assignment is exactly what registration would have created: tagged **Baseline**, pinned to the bundled file's SHA-256 and declared version. Restoring a manifest that is already assigned returns `409`, and so does restoring one whose policy names overlap another manifest assigned to the agent: those manifests are alternatives, not cumulative, so unassign the other one first.

Until you restore it, `kyvvu doctor` and the dashboard checklist report the agent as having no policies in effect, and every step it takes is allowed.

## Licensing — read this before assuming the trial is Apache-2.0

The bundled policy *content* is Apache-2.0, like the manifests repository it is copied from. That is why it ships in `kyvvu` (Apache-2.0) and not in `kyvvu-engine` (BSL 1.1): policy content must carry the licence of the manifests it came from.

Two consequences, both deliberate:

* **Apache-2.0 manifests do not make the trial Apache-licensed end to end.** The BSL 1.1 engine still evaluates them, so free use of the loaded baseline remains bounded by the BSL's non-production terms. See [Licensing](/reference/licensing.md).
* **`pip install kyvvu-engine` alone does not get the frozen set.** It is a `kyvvu` (SDK) feature. Install the SDK, which is what most users install anyway and which pulls the engine in as a dependency.

***

## Next steps

* [kyvvu try](/cli-reference/try.md) — run this set, or your own manifest, with no account
* [OWASP Default Template](/policy-authoring/owasp-default.md) — all 8 OWASP policies in detail
* [Manifests](/policy-authoring/templates.md) — the other manifests, and how assignment works
* [Licensing](/reference/licensing.md) — the Apache-2.0 / BSL 1.1 boundary
* [Built-in Rules Reference](/policy-authoring/rules-reference.md) — the rules these policies use
