Creating Policies¶
What you'll learn: How policies reach an agent (manifests assigned to it, or the API), how to target them at specific agents, and how to enable/disable them through the API.
Via the dashboard¶
The dashboard has no Policies page and no form for writing a single policy. Policies get to an agent two ways:
- Every agent gets the bundled baseline at registration (Claude Code agents get the Claude Code Safety variant). See The Bundled Baseline.
- You assign manifests. A manifest is a YAML file of policies in a connected repository. On the Manifests page connect a repository (the Kyvvu/manifests library is already connected in every new workspace) and browse its manifests; click Assign on a manifest and pick the agents. On the Agents page, expand an agent to update or remove an assignment or view the policies it created. Assigning a manifest creates the policies in it for that agent.
To write your own policy, add it to a manifest (see Manifests; a manifest policy can be limited to a risk class with risk_classification) and assign that manifest. To stop applying a manifest's policies, remove its assignment from the agent. Policy and assignment changes take effect within the policy TTL window (default: 5 minutes). No agent restart required.
The API below creates and edits individual policies directly. It is what the dashboard's assignment flow uses underneath, and it needs a signed-in user's token, not an agent API key.
Writing a manifest (YAML)¶
A manifest is the form most policies are written in: it is what the dashboard assigns, what kyvvu try --manifest and kyvvu serve --policy-file load, and what lives in a repository. The JSON bodies below (the API) and the YAML here carry the same fields.
| Key | Where | Required | Meaning |
|---|---|---|---|
name |
manifest | yes | A label for the set. |
description, version |
manifest | no | Free text; version is shown as provenance. |
policies |
manifest | yes | The list of policies. |
name |
policy | yes | Unique within the manifest. Two manifests assigned to one agent may share policy names; each copy is evaluated. (What one agent cannot hold is two live assignments of manifests with the same manifest name: 409.) |
rule_type |
policy | yes | A rule from the rules reference. |
params |
policy | per rule | The rule's parameters. Keys the rule does not know, or required keys that are missing, refuse the whole manifest. |
severity |
policy | yes | critical blocks; high, medium and low warn (the step still runs). |
enforcement_point |
policy | yes | step_execution (every step) or agent_registration (once). |
description |
policy | no | Free text. |
risk_classification |
policy | no | Apply only to agents of this class or above (minimal < limited < high). |
agent_id |
policy | no | Does nothing in a manifest. The key is accepted and then dropped when the manifest is read, so a policy carrying it applies to every agent (check: kyvvu try and kyvvu serve evaluate it for any agent_id). It is a field of the policy row, not of the manifest: a policy created with agent_id through the API applies only when the agent_id of the evaluation context equals it, and the platform sets it itself when it materialises an assigned manifest for one agent. To limit an offline policy to one agent, use a separate manifest file per agent. |
Here is a complete small manifest: one simple policy and one compound policy. It is the file below, run as written.
name: Support agent policies
description: |
One simple policy and one compound policy for a support agent.
version: "1.0.0"
policies:
# Simple: one rule, its params, where it runs, how severe a violation is.
- name: At most 2 CRM reads per task
description: Caps how many customer records one task may read.
rule_type: execution_max_steps
params:
kind: step.resource
step_name: read_crm
max_steps: 2
severity: critical # critical blocks; low, medium and high warn
enforcement_point: step_execution
# Compound: "block an outbound message after a PII read unless a human
# approved". The policy is the negation of the violation, so the top rule is
# not(all_of(...)): all_of passes when the dangerous situation holds, not
# turns that into a violation.
- name: Outbound message after a PII read needs human approval
rule_type: not
params:
condition:
rule_type: all_of
params:
conditions:
- rule_type: current_is
params: {kind: step.message, verb: POST}
- rule_type: history_contains
params:
kind: step.resource
count_attempts: true
verb: GET
property_filter: {data.classification: pii}
- rule_type: not
params:
condition:
rule_type: step_requires_gate
params:
target_kinds: ["step.message"]
target_verb: POST
gate_kind: human
max_steps_since_gate: 3
severity: critical
enforcement_point: step_execution
risk_classification: minimal # optional: this class and above
To try it with no agent and no account, describe a path and evaluate it:
agent:
agent_id: small-demo
risk_classification: minimal
allowed_tools: [read_crm, call_llm, send_email]
steps:
- step_name: read_crm
kind: step.resource
verb: GET
properties: {data: {classification: pii}}
- {step_name: call_llm, kind: step.model}
- step_name: send_email
kind: step.message
verb: POST
properties: {message: {recipient: "customer@example.com"}}
$ KV_ENVIRONMENT=development kyvvu try --manifest small.yaml --steps small-steps.yaml
kyvvu try — enforcement with no account, no API key and no network
Policy source small.yaml
Policies 2 offered, 2 enforcing (state=enforcing)
2 evaluated per step, 0 at agent registration
Agent small-demo risk=minimal tools: read_crm, call_llm, send_email
Environment development
Path small-steps.yaml
# DECISION RISK BEHAVIOUR VIOLATED POLICY
1 ALLOW 0.00 step.resource GET read_crm —
2 ALLOW 0.00 step.model call_llm —
3 BLOCK 1.00 step.message POST send_email Outbound message after a PII read needs human
approval (critical, block)
BLOCKED — send_email was stopped before it ran (risk 1.00)
Outbound message after a PII read needs human approval (critical)
{"sub_results": [{"rule_type": "all_of", "label": "all_of", "passed": true, "reason": "negation failed — inner condition passed"}]}
The same step, after an approval gate
recorded step.gate human_approval output.decision = "pass"
send_email is now ALLOW (risk 0.00)
Same step, same policies, different path.
A gate only counts when it carries a result.
Next
kyvvu try --steps path/to/steps.yaml run your own behaviour path
kyvvu serve --policy-file <manifest.yaml> same policies over HTTP (included with kyvvu-cli)
kyvvu init scaffold an agent that runs with no account
No trial record was written. Pass --record-trial to keep a local note of this run (opt-in, stays on
this machine, sent nowhere); --forget-trial deletes it.
kyvvu try exits 0 here although a step was blocked: see its exit codes. A field or property_filter value in a policy is a path into the step, with its own small grammar: see Field and property paths.
The policy from the API example below, as a manifest entry:
name: Required request reason
version: "1.0.0"
policies:
- name: Requests need a reason
rule_type: field_not_empty
params:
field: input.reason
severity: critical
enforcement_point: step_execution
Your manifest beside the baseline, offline¶
A script governed with no platform account takes its policies from policies=. To enforce the 15 bundled baseline policies and a manifest of your own together, add the two lists. load_manifest_policies, exported by the kyvvu client, loads a manifest file the way kyvvu try --manifest does and refuses an invalid one. Save this manifest 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
and run this program beside it:
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 (the EU AI Act chatbot disclosure policy, high) and still goes out. The second is blocked by your own critical policy, and the baseline's finding is reported on it beside yours. 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)
Policy names may repeat across the two lists: a policy of yours that has the same name as a baseline policy loads beside it, and both are evaluated.
KV_POLICY_FILE is the other offline policy source, and it replaces the baseline rather than adding to it: with KV_POLICY_FILE=my-policies.yaml and no policies=, only the manifests it names are enforced. To keep the baseline as well, build policies= as above.
Via the API¶
Set KV_USER_TOKEN to your signed-in user JWT. KV_API_URL defaults to
https://platform.kyvvu.com; set it for another deployment.
curl -X POST "${KV_API_URL:-https://platform.kyvvu.com}/api/v1/policies" \
-H "Authorization: Bearer $KV_USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Requests need a reason",
"rule_type": "field_not_empty",
"params": {"field": "input.reason"},
"enforcement_point": "step_execution",
"severity": "critical",
"enabled": true
}'
Response excerpt (201). Use the returned id for update and delete requests:
{
"id": 1,
"name": "Requests need a reason",
"rule_type": "field_not_empty",
"params": {
"field": "input.reason"
},
"enforcement_point": "step_execution",
"severity": "critical",
"enabled": true,
"agent_id": null,
"risk_classification": null
}
Targeting an agent¶
Send this body with the same POST command. Replace agent_id with your
agent’s platform id:
{
"name": "Per-task $5 LLM budget",
"rule_type": "usage_budget",
"params": {"kind": "step.model", "property_path": "usage.cost_usd", "budget": 5.0},
"enforcement_point": "step_execution",
"severity": "high",
"agent_id": "agent_3f9c2a7b1d4e6f80"
}
Targeting a risk classification¶
Alternatively, use this POST body to target a risk class:
{
"name": "High-risk agents: working hours only",
"rule_type": "working_hours_only",
"params": {"start_hour": 8, "end_hour": 18, "timezone": "Europe/Amsterdam"},
"enforcement_point": "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):
curl "${KV_API_URL:-https://platform.kyvvu.com}/api/v1/policies/rules" \
-H "Authorization: Bearer $KV_USER_TOKEN"
Each rule includes its description, parameter schema, and applicable enforcement points.
Note:
kyvvu list-policieslists 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:
curl -X PUT "${KV_API_URL:-https://platform.kyvvu.com}/api/v1/policies/1" \
-H "Authorization: Bearer $KV_USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled": false}'
Disabled policies are skipped during evaluation. Re-enable by setting enabled: true.
Deleting policies¶
curl -X DELETE "${KV_API_URL:-https://platform.kyvvu.com}/api/v1/policies/1" \
-H "Authorization: Bearer $KV_USER_TOKEN"
Deletion is a soft delete: the policy is marked as deleted and disabled, and kept so the incidents that reference it stay intact.
Next steps¶
- OWASP Default Template — the 8 pre-built security policies
- Compound Policies — combine rules with
all_of,any_of,not - Built-in Rules Reference — all 25 rules and their parameters