Veefore docs

Write your first policy

A real refund policy, built one rule at a time, with the reasoning behind each rule and the tests that keep it honest.

We will build a policy for a refund agent. Four rules, added in order of how much they matter, each one justified. By the end you will have something you can activate and a test suite that stops a future edit from breaking it.

#Start from a template

bash
veefore policies list

The built-in templates are complete, valid rule sets you create a policy from and then edit. They are starting points, not managed policies — once created, the rules are yours and Veefore will not change them underneath you.

starter-observe-safe-defaults is the one to begin with. It allows read-style capabilities, requires approval for anything flagged dangerous, and blocks destructive database operations. All templates with their full rule sets are in Policy templates.

#Rule 1 — block what must never happen

Start with the absolutes. These are not conditional on amounts or risk; they are things that are simply not allowed through an agent, ever.

json
{
  "id": "rule-block-destructive-db",
  "name": "Block destructive database operations",
  "description": "DELETE, DROP and TRUNCATE are never permitted through an agent.",
  "priority": 900,
  "enabled": true,
  "when": {
    "kind": "group",
    "id": "g-destructive",
    "operator": "and",
    "children": [
      {
        "kind": "condition",
        "id": "c-cap-db",
        "field": "capability",
        "operator": "starts_with",
        "value": "database."
      },
      {
        "kind": "condition",
        "id": "c-op-destructive",
        "field": "parameters",
        "path": "operation",
        "operator": "in",
        "value": ["DELETE", "DROP", "TRUNCATE"],
        "ignoreCase": true
      }
    ]
  },
  "then": {
    "decision": "BLOCK",
    "reason": "Destructive database operations are not permitted through an agent."
  }
}

Two things to notice. path: "operation" reaches into parameters.operation — that is how you test a request parameter rather than a top-level field. ignoreCase: true means delete and DELETE both match, which matters because you do not control how the model capitalises things.

Why priority 900 when strictness wins anyway?

A BLOCK would win regardless of priority. Priority orders evaluation and the explanation, so the absolutes appear first in the trace and are the first thing a reader sees. Convention: absolutes 900+, escalations 500–800, allowances below.

#Rule 2 — escalate what needs a human

json
{
  "id": "rule-refund-ceiling",
  "name": "Approval above ₹1,000",
  "description": "Larger refunds get a human decision.",
  "priority": 700,
  "enabled": true,
  "when": {
    "kind": "group",
    "id": "g-large-refund",
    "operator": "and",
    "children": [
      {
        "kind": "condition",
        "id": "c-cap-refund",
        "field": "capability",
        "operator": "eq",
        "value": "refund.create"
      },
      { "kind": "condition", "id": "c-amount", "field": "amount", "operator": "gt", "value": 100000 }
    ]
  },
  "then": {
    "decision": "REQUIRE_APPROVAL",
    "reason": "Refunds above ₹1,000 require a human approver.",
    "approval": {
      "routingMode": "SINGLE",
      "requiredApprovals": 1,
      "approverRoleKeys": ["APPROVER"],
      "ttlSeconds": 900
    }
  }
}

amount is in minor units, so 100000 is ₹1,000.00. Getting this wrong by a factor of 100 is the most common policy bug there is, in both directions.

ttlSeconds: 900 means fifteen minutes to respond. Set it to how fast a human can realistically respond to this particular thing, not to a round number. An approval that always expires is a rule that always blocks.

#Rule 3 — use risk, not just thresholds

A fixed threshold cannot see that a request is unusual. Risk can.

json
{
  "id": "rule-new-destination-large",
  "name": "Block large amounts to unseen destinations",
  "priority": 820,
  "enabled": true,
  "when": {
    "kind": "group",
    "id": "g-new-dest",
    "operator": "and",
    "children": [
      {
        "kind": "condition",
        "id": "c-signal",
        "field": "risk.signals",
        "operator": "contains",
        "value": "UNUSUAL_DESTINATION"
      },
      { "kind": "condition", "id": "c-amt", "field": "amount", "operator": "gt", "value": 50000 }
    ]
  },
  "then": {
    "decision": "BLOCK",
    "reason": "Large amounts to a destination this agent has never used are refused."
  }
}

Testing a named signal says what you mean. risk.score >= 70 is a number; risk.signals contains UNUSUAL_DESTINATION is a sentence an operator can read back to you. Prefer the signal when you know which one you care about, and the score when you want a general safety net.

#Rule 4 — mask what the agent does not need

json
{
  "id": "rule-mask-pii",
  "name": "Mask customer contact details",
  "priority": 300,
  "enabled": true,
  "when": {
    "kind": "condition",
    "id": "c-order-read",
    "field": "capability",
    "operator": "eq",
    "value": "order.read"
  },
  "then": {
    "decision": "MASK",
    "reason": "Agent does not need customer contact details to process a refund.",
    "maskFields": ["customer.email", "customer.phone", "customer.address"]
  }
}

MASK lets the action through with those paths redacted — in the forwarded request and in the audit trail. The agent does its job; the PII never enters its context.

This is the rule people underuse. An agent that does not receive data cannot leak it, and masking costs nothing in capability.

#Set the default

json
{
  "schemaVersion": 1,
  "defaultDecision": "ALLOW",
  "defaultReason": "No rule matched; the request is within the refund policy baseline.",
  "rules": [ … ]
}

`defaultDecision: BLOCK` denies everything in scope

Deny-by-default is the right instinct and it interacts badly with broad scope. Because strictness wins over priority, a BLOCK default bound to an entire environment beats every ALLOW from every other policy in that environment. Unrelated agents stop working and no rule looks relevant.

If you want deny-by-default, bind the policy to the AGENT or TOOL it constrains — not to the environment. See Tenancy.

#Write the tests

Test cases live on the policy version. Given an input, Then expect a decision.

Given Then
refund.create, amount 50000 ALLOW
refund.create, amount 150000 REQUIRE_APPROVAL
refund.create, amount 100000 ALLOW — the boundary is gt, not gte
refund.create, amount 100001 REQUIRE_APPROVAL
database.query, operation: DELETE BLOCK
database.query, operation: delete BLOCK — case-insensitivity holds
database.query, operation: SELECT ALLOW
order.read MASK
amount 60000 with UNUSUAL_DESTINATION BLOCK
bash
veefore policies test

Test the boundaries, not the middle. 100000 and 100001 catch an off-by-one between gt and gte; 50000 tells you almost nothing. And test the case-insensitivity claim explicitly, or you will not notice when someone removes ignoreCase.

#Simulate against real traffic

bash
curl -X POST https://veefore.example.com/v1/policies/simulate \
  -H "Authorization: Bearer $VEEFORE_API_KEY" \
  -H 'content-type: application/json' \
  -d '{
    "agentId": "agt_01JAV…",
    "tool": "refund_customer",
    "capability": "refund.create",
    "amount": 150000
  }'

Returns the decision with its full explanation — the deciding rule, every rule that matched, the resolved authority, the risk signals. No side effects of any kind.

veefore deploy preflight does this across your recent observed traffic automatically, which is a much better signal than a handful of cases you thought of.

#Activate

bash
veefore policies deploy pol_01ABC --version 4 --reason "Refund policy reviewed in CHG-4411"

Three requirements, none optional:

  1. policy:activate — high impact, cannot come from a wildcard grant
  2. Step-up authentication, regardless of session age
  3. Every test case passing

There is no override flag for a failing test, because the one time you want an override is the one time you should not have one.

#Iterate

Policies are versioned and immutable once activated.

bash
veefore policies diff pol_01ABC --from 4 --to 5
veefore policies deploy pol_01ABC --version 4 --reason "Rolling back CHG-4411"

Rollback creates a new version with the old content rather than moving a pointer, so history stays linear and the audit trail reads as a sequence of decisions.

Identity, authority, policy and enforcement for autonomous AI agents.