For the complete documentation index, see llms.txt. This page is also available as Markdown.

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:

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:

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:

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:

Those three asserts 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

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, 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?":

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.

  • 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

Last updated