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¶
This needs the kyvvu command (uv tool install kyvvu-cli); the scaffolded
project installs the Python client from its requirements.txt. See
Installation.
kyvvu init my-agent
This scaffolds project files — agent.py, requirements.txt, .env.example, .gitignore, README.md, and AGENTS.md (instructions for coding agents working in the project, kept if one exists). No authentication is required.
The output looks like:
✓ Created my-agent/
Next steps:
cd my-agent
# Install requirements:
pip install -r requirements.txt
# Run the agent -- no account needed:
python agent.py
With no KV_API_KEY it runs offline against Kyvvu's bundled baseline
(15 policies, OWASP + EU AI Act) and prints its trace here.
To connect it to the platform, set your key and run it again:
# (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
Registering applies the same baseline on the platform.
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>
Run it with no account¶
python agent.py works straight after pip install -r requirements.txt. With
no KV_API_KEY set, the scaffolded agent does not register: it builds its
client with an agent_id and Kyvvu's bundled baseline
(load_baseline_policies(), the same 15 policies the platform applies),
prints its trace in the terminal, and exits 0. The send_reply warning and the
run_script block described below appear in that
trace, and the run's first lines name the one change that connects it to the
platform: set KV_API_KEY and run it again.
Get an API key¶
To connect the agent to the platform it needs a KV_API_KEY: with one,
agent.py registers instead of running offline. 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 for the full explanation.
Run the demo agent¶
Each step's kind field identifies its action kind; ActionKind.step_model
is the model-call value used by the demo agent.
cd my-agent
pip install -r requirements.txt
export KV_API_KEY=KvKey-...
export KV_ENVIRONMENT=development # optional, but see the note below
python agent.py
Set KV_ENVIRONMENT=development for local work. The environment an agent
registers under is the environment= argument, then KV_ENVIRONMENT, then
production, and the default is production everywhere. The scaffolded agent
sets nothing, so without the variable your trial run registers as a production
agent. kyvvu init puts KV_ENVIRONMENT=development in .env.example.
That is the whole first run. There is no assignment step before it — see
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:
"""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:
python agent.py
With no KV_API_KEY it needs no account: it governs itself offline against
Kyvvu's bundled baseline and prints its trace here. To connect it to the
platform, set a key and run it again -- that is the only change:
export KV_API_KEY=KvKey-... # dashboard -> Workspace -> API Keys
export KV_ENVIRONMENT=development # unset means "production"
python agent.py
"""
from __future__ import annotations
import os
import sys
from kyvvu import Kyvvu, KyvvuBlockedError, KyvvuConfigError, RiskClassification, ActionKind, Verb
from kyvvu_baseline import load_baseline_policies
# The key decides the path: with one the agent registers on the platform,
# which applies the baseline and stores the trace; without one it runs
# offline against the same bundled baseline, with no account.
ONLINE = bool(os.environ.get("KV_API_KEY"))
if ONLINE:
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", "send_reply", "run_script"],
)
else:
kv = Kyvvu(
agent_id="my-agent",
risk_classification=RiskClassification.MINIMAL,
# The same tools register_agent() declares above.
declared_tools=["call_llm", "fetch_user_data", "send_reply", "run_script"],
policies=load_baseline_policies(),
)
print(
"No KV_API_KEY set: running offline against Kyvvu's bundled baseline\n"
"(15 policies, OWASP + EU AI Act). No account needed; the trace prints here.\n"
"To connect this agent to the platform, set KV_API_KEY (dashboard ->\n"
"Workspace -> API Keys) and run it again.\n"
)
@kv.step(ActionKind.step_model)
def call_llm(prompt: str) -> str:
"""Mocked model call -- returns a canned response."""
return f"(mocked response to: {prompt!r})"
@kv.step(ActionKind.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(ActionKind.step_message, Verb.POST)
def send_reply(to: str, body: str) -> str:
"""Mocked outbound message -- draws a policy warning, not a block.
The model's answer reaches a person here with no gate after the model
call, so two EU AI Act policies warn: "AI-generated content must be
marked before delivery" (Art. 50(2)) and "Chatbot disclosure gate
required before outbound messages" (Art. 50(1)). That is a compliance
finding: the step still runs, and the warning is recorded on the trace.
"""
print(f"(would send to {to}: {body!r})")
return "sent"
@kv.step(ActionKind.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
output["decision"] is "pass". *Anywhere* earlier in the task
counts -- check_step_requires_gate scans the whole history, and a bare
step.gate with no decision 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 draws a warning -- model output delivered with no gate after it:
send_reply("alice@example.com", answer)
# 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
def explain_block(exc: KyvvuBlockedError) -> None:
"""Print what would clear this block, chosen by the rule that fired.
A gate clears a gate rule and nothing else: it does not put a step on
an allowlist or a host on a domain list. So the advice is picked per
violated policy, by its rule type, rather than printed once for all.
"""
print(
"\nThis is Kyvvu working as intended. A block never ends the task:\n"
"the blocked step is recorded and the task stays open, so an agent\n"
"can resolve the block and carry on."
)
violated = [p for p in exc.result.policies if p.violated]
rule_types = {p.rule_type for p in violated}
advised = False
if "step_requires_gate" in rule_types:
advised = True
print(
"Record the approval as a gate -- it only counts when it carries\n"
"a pass decision -- then retry in the same task:\n"
" kv.add_step(ActionKind.step_gate, \"human_approval\",\n"
" properties={\"kind\": \"human\"}, output={\"decision\": \"pass\"})"
)
if "step_name_in_allowlist" in rule_types:
advised = True
print(
f"'{exc.step_name}' is not in the declared_tools list passed to\n"
"kv.register_agent(). Add it there if the agent should use it."
)
if "domain_allowlist" in rule_types:
advised = True
print(
"The step's properties.target.host is not on the policy's\n"
"allowed_domains -- and a step that records no host at all is blocked\n"
"too. Call an approved host, or assign your own copy of the manifest\n"
"with your domains in it."
)
if any(p.name == "no_policies_available" for p in violated):
advised = True
print(
"No policies could be loaded, so every step is blocked (fail closed).\n"
"The KYVVU line printed above says why -- check KV_API_KEY first."
)
if not advised:
print(
"The policy's description says what it requires: find it in the\n"
"manifest that defines the policy, on the dashboard's Manifests page."
)
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})")
explain_block(exc)
# This script gives up instead, so it closes the task itself.
try:
kv.error_task(error=exc)
except KyvvuConfigError:
pass # Already closed, or the block was on task.start (never opened)
# Offline, like `kyvvu try`, the block is the verdict the run exists
# to show, so it exits 0. Online, the agent did not finish its task.
sys.exit(1 if ONLINE else 0)
except Exception as exc:
try:
kv.error_task(error=exc)
except KyvvuConfigError:
pass # No task to close (start_task itself failed)
raise
else:
kv.end_task()
It starts a task and runs four steps. The third one, send_reply, draws a
warning and still runs; the fourth, run_script, is blocked. This transcript
omits the requirements-installer output, timestamps, and warning source paths
printed to stderr; identifiers
are illustrative:
kyvvu.core INFO Agent registered: agent_id=agent_3f9c2a1b7d4e8f60
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.50
kyvvu_engine.io.runner INFO policy violation: task=d4c09846-8aa step='send_reply' action=warn risk=0.75 (2 violations)
(would send to alice@example.com: '(mocked response to: "User Alice asks: What\'s the weather?")')
kyvvu_engine.io.runner INFO policy violation: task=d4c09846-8aa step='run_script' action=block risk=1.00 (1 violations)
⛔ Policy blocked this step: Step 'run_script' blocked (risk_score=1.00): Code execution requires a preceding gate (manifest OWASP Top 10 for Agentic Applications - Default Security Template)
Risk score: 1.00
Action: block
• Code execution requires a preceding gate (critical)
This is Kyvvu working as intended. A block never ends the task:
the blocked step is recorded and the task stays open, so an agent
can resolve the block and carry on.
Record the approval as a gate -- it only counts when it carries
a pass decision -- then retry in the same task:
kv.add_step(ActionKind.step_gate, "human_approval",
properties={"kind": "human"}, output={"decision": "pass"})
kyvvu_engine.io.runner INFO task d4c09846-8aa ended — 6 steps (task.start×1, step.resource×1, step.model×1, step.message×1, step.exec×1, task.error×1)
kyvvu_engine.io.exporters.http INFO HttpExporter: batch accepted task=d4c09846-8aa0-4bfe-9ace-b2d541adc67f steps=6 hash_tail=97eff6b45b9150bf…
The advice under the block is chosen by the rule of the policy that fired,
because a gate clears only a gate rule. A step_requires_gate block prints the
gate above; a step_name_in_allowlist block tells you to add the step to
declared_tools; a domain_allowlist block tells you the step's host is not
approved; anything else points you to the policy's description.
HttpExporter: batch accepted is the line that matters for onboarding. It
comes last because the task is sent when it closes, and the script closes it
with kv.error_task() after printing the advice. It means 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 baseline is on by default, not mandatory: you can unassign either
manifest from the dashboard. Registering again will not put it back, because
the platform honours the choice you made. To restore it, use the dashboard's
Manifests page or the agent's page, or POST /api/v1/assignments with the
bundled manifest path; see
Removing and restoring the baseline.
When the platform cannot be reached¶
The first run needs the platform. register_agent() is what creates the
agent, and with no cached registration there is no identity to fall back on,
so registration fails closed: it retries three times (about four seconds),
then raises and no step runs. Pointing the scaffold at a closed port
(KV_API_URL=http://127.0.0.1:9) ends with:
kyvvu.exceptions.KyvvuAPIError: register_agent connection error: [Errno 111] Connection refused (POST http://127.0.0.1:9/api/v1/agents). There is no cached registration for agent_key 'my-agent' to fall back on, so the agent cannot start: a first run needs the platform. Check KV_API_URL (api_url) and your network. To run without the platform on purpose, pass agent_id= and policies= (or set KV_POLICY_FILE) instead of calling register_agent(), or try `kyvvu try`.
A timeout raises KyvvuTimeoutError (a subclass of KyvvuAPIError) with the
same advice. Nothing is written to the registration cache, so the next run is
still a first run.
Later runs survive an outage. A successful registration is cached at
~/.kyvvu/registrations/<agent_key>.json. If the registration payload, URL
and key are unchanged, the next run reuses that agent_id without contacting the platform
at all; if it changed, the Python client retries and then falls back to the cached
agent_id with a warning. Either way the registration policies are evaluated
locally against the metadata you pass, so a blocking one still stops the agent.
The cache lives under HOME, so a container with an ephemeral home directory
starts every run as a first run.
What is then enforced depends on the policy fetch, which fails too:
- With
KV_POLICY_CACHE_PATHset (policy_cache_path=) and a cache file written by an earlier successful fetch, the platform's last answer — every manifest assigned to the agent, not just the baseline. - Otherwise, the Python client's bundled copy of the 15 baseline policies, with a
warning (
Loaded 15 inline policies as a fallback). That is the baseline even if you have assigned more manifests or unassigned the baseline on the platform; setKV_POLICY_CACHE_PATHif that difference matters to you.
kv.policy_status() reports either as load_state=stale_cache, and the next
successful fetch replaces the set with the platform's answer. A long-running
agent that already holds a set keeps enforcing it through an outage. Steps and
incidents from a task that ends during the outage are not delivered: each
batch fails with an HttpExporter: network error warning and is not retried.
To run without the platform on purpose, do not call register_agent().
Give the client its identity and its policies directly, and leave out
agent_key, so the platform is not the policy source even with KV_API_KEY
exported:
from kyvvu import Kyvvu
from kyvvu_baseline import load_baseline_policies
kv = Kyvvu(
agent_id="my-agent",
policies=load_baseline_policies(), # KV_POLICY_FILE=manifest.yaml replaces this list
declared_tools=["call_llm", "fetch_user_data", "send_reply", "run_script"],
)
The trace then goes to stdout. kyvvu try does the same with a built-in demo
agent and no code; see kyvvu try.
That enforces the baseline alone. To enforce a manifest of your own as well,
add its policies to the list: load_manifest_policies, exported by the kyvvu
client, loads a manifest file and refuses an invalid one. With this manifest
saved as my-policies.yaml:
name: My support policies
version: "1.0.0"
policies:
- name: At most one reply per task
description: Blocks a second reply in the same task.
rule_type: execution_max_steps
params:
kind: step.message
max_steps: 1
severity: critical # critical blocks; low, medium and high warn
enforcement_point: step_execution
this program enforces all 16 policies:
from kyvvu import ActionKind, Kyvvu, KyvvuBlockedError, load_manifest_policies
from kyvvu_baseline import load_baseline_policies
kv = Kyvvu(
agent_id="my-agent",
# The 15 bundled baseline policies, then every policy in your own manifest.
policies=load_baseline_policies() + load_manifest_policies("my-policies.yaml"),
declared_tools=["send_reply"],
log_location="none",
)
@kv.step(ActionKind.step_message, verb="POST")
def send_reply(text: str) -> str:
return f"sent: {text}"
task = kv.start_task()
try:
print(send_reply("Your order has shipped."))
print(send_reply("Anything else I can help with?"))
except KyvvuBlockedError as error:
print(f"blocked: {error}")
finally:
kv.end_task(task)
The first reply draws a warning from the baseline's EU AI Act chatbot disclosure
policy and still goes out; the second is blocked by your own critical policy,
with the baseline's finding reported beside it. The output, without timestamps
and the warning's source line printed to stderr:
kyvvu_engine.io.runner INFO Loaded 16 policies from the local policy source (inline policies=).
kyvvu_engine.io.runner INFO policy violation: task=306d811e-efb step='send_reply' action=warn risk=0.75 (1 violations)
sent: Your order has shipped.
kyvvu_engine.io.runner INFO policy violation: task=306d811e-efb step='send_reply' action=block risk=1.00 (2 violations)
blocked: Step 'send_reply' blocked (risk_score=1.00): At most one reply per task (manifest My support policies); Chatbot disclosure gate required before outbound messages (manifest EU AI Act - Minimal / Limited Risk Compliance)
log_location="none" keeps the trace out of that output. A policy of yours may
share its name with a baseline policy: both load and both are evaluated.
KV_POLICY_FILE replaces the baseline rather than adding to it: with it set and
no policies=, only the manifests it names are enforced. To keep the baseline,
build policies= as above. See
Writing a manifest for
what a manifest can hold.
Why the demo agent shows compliance warnings¶
At registration, the baseline's EU AI Act set asks every agent to name an
accountable owner, and the scaffold passes no owner_id, so "Agent must have a
designated owner" reports a warning (medium, risk 0.50 — not a block; the
agent runs normally). Registration policies are evaluated against the metadata
your code passes, whether or not the platform stores it, so the fix is to pass
owner_id= to register_agent(); store_metadata decides only whether the
platform keeps those fields.
At send_reply, the model's answer is delivered to a person with no gate
recorded after the model call, so two EU AI Act Article 50 policies warn
(high, risk 0.75): the content is not marked as AI-generated (Art. 50(2)),
and the person was not told they are dealing with an AI (Art. 50(1)). That is a
compliance finding: the step runs and the finding is recorded. A passing
step.gate between call_llm and send_reply satisfies both.
The block on run_script is different in kind — a security stop (OWASP
ASI05): code execution with no approval in front of it never runs.
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 four,
one per violated policy:
| incidents | why | do they pile up? |
|---|---|---|
| "Agent must have a designated owner" | the scaffold passes no owner_id, explained above |
no — a repeat updates the open incident for the same agent, policy and environment |
| "AI-generated content must be marked before delivery" and "Chatbot disclosure gate required before outbound messages" | send_reply delivers model output with no gate after the model call |
no — step_execution violations are grouped the same way: each repeat raises the open incident's occurrence_count and moves its last_occurrence instead of opening another |
| "Code execution requires a preceding gate" | the demo's punchline | no, same reason |
An incident stays open, and keeps absorbing repeats, while it is active or ignored; once you resolve it, the next violation opens a new one. See Incidents.
To stop the registration incident, pass owner_id= to register_agent(). To
stop the two delivery incidents, record a passing gate between call_llm and
send_reply; to stop the last, a passing gate before run_script — or resolve
them, which is what the Incidents page is for.
The model call itself opens none: the EU AI Act content-marking policy asks for
a gate between model output and its delivery (an outbound step.message
POST, or a step.resource POST to an external target), not before the model
is called.
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).
Note
Caching:
register_agent()results are cached locally at~/.kyvvu/registrations/. If you call it on every startup with the same parameters, the Python client reuses the cached registration without a network call. You can safely callregister_agent()every time your agent starts.Note
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=Trueto opt in to storing metadata on the platform.
What happened¶
The @kv.step decorator wraps each function call in a three-phase cycle:
- Evaluate — the engine checks the intended behaviour against all applicable policies. It reads the task's history to make path-dependent decisions.
- Execute — if the evaluation returns
alloworwarn, the function runs. - Record — the completed step (with output) is appended to the task's history for future evaluations.
Steps 1 and 2 passed for fetch_user_data and call_llm, and for send_reply too: a warn still executes, and only records the finding. 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 returnedblock. - The decorator raised
KyvvuBlockedErrorinstead of executing the function.
Clearing the block¶
A bare step.gate is not enough. step_requires_gate matches on a completed
gate's output.decision, which defaults to "pass" in the policy. A gate with
no output, or with a denied decision, leaves the block exactly where it was:
| gate recorded in task history | verdict |
|---|---|
| none | block (1.00) |
step.gate with no properties |
block (1.00) |
step.gate, properties={"kind": "human"}, no output |
block (1.00) |
step.gate, properties={"kind": "human"}, output={"decision": "deny"} |
block (1.00) |
step.gate, properties={"kind": "human"}, output={"decision": "pass"} |
allow (0.00) |
kv.add_step() adds properties.kind="human" for a gate when you omit it.
The working remedy — the one the demo prints — records the decision:
kv.add_step(
ActionKind.step_gate,
"human_approval",
properties={"kind": "human"},
output={"decision": "pass"},
)
run_script("save_response('...')") # now allowed
See kv.add_step().
Note
A decorated gate passes unless it raises
GateDenied.
@kv.step(kind="step.gate")does not read the wrapped function's return value. The decorator records the gate's decision itself, after the function finishes:passwhen it returns normally,denywhen it raiseskyvvu.GateDenied(or any other exception). Measured against the bundled baseline:
gate recorded by the decorator verdict no gate at all block(1.00)@kv.step(ActionKind.step_gate)whose function raisesGateDeniedblock(1.00)@kv.step(ActionKind.step_gate)whose function returns normallyallow(0.00)So a human-approval function must deny by raising:
from kyvvu import GateDenied, ActionKind @kv.step(ActionKind.step_gate, properties={"kind": "human"}) def human_approval(action: str) -> None: if not ask_human(action): raise GateDenied(f"{action} refused")A function that only prints "denied" or returns
Falserecords a pass, because a gate passes unless it raisesGateDenied.GateDeniedis not aKyvvuBlockedError: a block is a policy's verdict on a step, a denial is the gate function's own decision. When you would rather state the result yourself,kv.add_step()records the decision you give it, as above.
"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¶
kyvvu list-policies --agent-id my-agent
Without --agent-id the command lists every policy in the workspace. Because
of the baseline, this is not empty on a fresh agent — 15 policies, before
you assign anything:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓
┃ NAME ┃ ENFORCEMENT POINT ┃ RULE_TYPE ┃ SEVERITY ┃ SOURCE ┃ ENABLED ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩
│ Agent must declare a substantive purpose │ agent_registration │ field_matches_regex │ high │ kyvvu-manifests-public/manifests/security/owasp-agen… │ ✓ │
│ Agent must declare a tool allowlist │ agent_registration │ field_not_empty │ high │ kyvvu-manifests-public/manifests/security/owasp-agen… │ ✓ │
│ Agent must declare a valid risk classification │ agent_registration │ field_in_list │ critical │ kyvvu-manifests-public/manifests/security/owasp-agen… │ ✓ │
│ Tool calls must be in the declared allowlist │ step_execution │ step_name_in_allowlist │ critical │ kyvvu-manifests-public/manifests/security/owasp-agen… │ ✓ │
│ Code execution requires a preceding gate │ step_execution │ step_requires_gate │ critical │ kyvvu-manifests-public/manifests/security/owasp-agen… │ ✓ │
│ Destructive resource operations require a preceding │ step_execution │ step_requires_gate │ critical │ kyvvu-manifests-public/manifests/security/owasp-agen… │ ✓ │
│ gate │ │ │ │ │ │
│ External content taint: high-impact actions require a │ step_execution │ not │ critical │ kyvvu-manifests-public/manifests/security/owasp-agen… │ ✓ │
│ fresh gate │ │ │ │ │ │
│ Bound resource calls per task to prevent runaway │ step_execution │ execution_max_steps │ high │ kyvvu-manifests-public/manifests/security/owasp-agen… │ ✓ │
│ loops │ │ │ │ │ │
│ Agent must have a documented purpose │ agent_registration │ field_not_empty │ high │ kyvvu-manifests-public/manifests/compliance/eu-ai-ac… │ ✓ │
│ Agent must have an identifying name │ agent_registration │ field_not_empty │ high │ kyvvu-manifests-public/manifests/compliance/eu-ai-ac… │ ✓ │
│ Risk classification must be valid │ agent_registration │ field_in_list │ critical │ kyvvu-manifests-public/manifests/compliance/eu-ai-ac… │ ✓ │
│ Agent must have a designated owner │ agent_registration │ field_not_empty │ medium │ kyvvu-manifests-public/manifests/compliance/eu-ai-ac… │ ✓ │
│ Agent purpose must be substantive │ agent_registration │ field_matches_regex │ medium │ kyvvu-manifests-public/manifests/compliance/eu-ai-ac… │ ✓ │
│ AI-generated content must be marked before delivery │ step_execution │ not │ high │ kyvvu-manifests-public/manifests/compliance/eu-ai-ac… │ ✓ │
│ Chatbot disclosure gate required before outbound │ step_execution │ step_requires_gate │ high │ kyvvu-manifests-public/manifests/compliance/eu-ai-ac… │ ✓ │
│ messages │ │ │ │ │ │
└───────────────────────────────────────────────────────┴────────────────────┴────────────────────────┴──────────┴───────────────────────────────────────────────────────┴─────────┘
The SOURCE column shows the path of the manifest each policy came from.
Table wrapping depends on terminal width.
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_kinds: ["step.exec"]. The same rule backs "Destructive resource operations require a preceding gate" with different parameters (target_kinds: ["step.resource"], target_verb: "DELETE").
For the full details of each policy, see The Bundled Baseline and OWASP Default Manifest.
Add more policies¶
This step is optional, and it comes after the first run — the platform knows
your agent only once it has registered, and kyvvu assign-manifest needs it to.
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:
- Go to the Manifests page in the dashboard
Kyvvu/manifestsis 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.- Click Assign on a manifest and select your agent
Or use the CLI. --agent-id takes the agent key the scaffold registers with
(my-agent) or the agent_… id that kyvvu list-agents prints. Replace the
example repository id 1 with the id shown by list-manifests:
kyvvu list-manifests
kyvvu assign-manifest --agent-id my-agent --repo-id 1 --manifest manifests/operational/demo.yaml
It answers with the assignment id, which kyvvu unassign-manifest
--assignment-id <id> takes to remove the manifest again (kyvvu
list-assignments shows it too, in the ID column):
✓ Assigned 'Kyvvu Demo' to agent agent_3f9c2a1b7d4e8f60 (2 policies created). Assignment id: 12
Kyvvu Demo layers cleanly on the scaffold: it adds "Agent purpose required"
and "Max LLM calls per task" (10), the demo agent satisfies both, and the next
run ends on the same run_script block as before.
Note
Not every library manifest fits the demo agent as published.
manifests/security/data-exfiltration-guard.yamlblocks the scaffold on its first step:
⛔ Policy blocked this step: Step 'fetch_user_data' blocked (risk_score=1.00): Restrict outbound to approved domains (manifest Data Exfiltration Guard) Risk score: 1.00 Action: block • Restrict outbound to approved domains (critical) This is Kyvvu working as intended. A block never ends the task: the blocked step is recorded and the task stays open, so an agent can resolve the block and carry on. The step's properties.target.host is not on the policy's allowed_domains -- and a step that records no host at all is blocked too. Call an approved host, or assign your own copy of the manifest with your domains in it.Its
allowed_domainsare placeholders (internal.example.com,api.example.com), anddomain_allowlistfails closed on astep.resourcethat records noproperties.target.host— which the mockedfetch_user_datadoes not. The library copy cannot be edited, so to use it, copy it into your own repository, list your real domains, and assign that copy instead.Warning
Distinct manifests can layer on one agent. Each manifest keeps and enforces its own policies, even when another manifest uses the same policy name.
The two manifests Kyvvu applies automatically,
owasp-agentic-default.yamlandeu-ai-act-minimal.yaml, are the same files published inKyvvu/manifests. Assigning a repository copy of either baseline manifest returns409because its manifest name is already assigned; the CLI prints the platform's reason on one line, startingError (409 manifest_already_assigned):, and exits 1. A manifest with a different name, such aseu-ai-act-high-risk.yaml, layers on the baseline and keeps both sources' policies active.To manage a baseline manifest from your own repository instead — to edit it, for instance — unassign the matching baseline manifest from the agent first.
See Manifests for details on manifest structure and available policy sets.
Next steps¶
- Understanding the Output — learn what each field in the behavioral trace means
- The Bundled Baseline — the 15 policies every agent starts with
- OWASP Default Template — the 8 OWASP policies explained in detail
- Python Decorator — full reference for
@kv.step