Skip to content

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:

  1. Every agent gets the bundled baseline at registration (Claude Code agents get the Claude Code Safety variant). See The Bundled Baseline.
  2. 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-policies lists 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