---
title: Policy
description: Deterministic, total, versioned rules over a closed field set — and the exact precedence that decides when several of them match.
---

A policy is a set of rules. Each rule has a condition expression, a priority,
and an effect. Evaluation is **deterministic** (the same input always yields the
same output) and **total** (every operator is defined for every input, and an
unresolvable comparison is `false` rather than an exception).

Those two properties are the product. A policy engine that can throw, or that
can decide differently on a Tuesday, is not something you can put in front of
production.

## The rule set

```json
{
  "schemaVersion": 1,
  "defaultDecision": "ALLOW",
  "defaultReason": "No rule matched; the request is within the baseline.",
  "rules": [
    {
      "id": "rule-refund-ceiling",
      "name": "Approval above ₹1,000",
      "description": "Larger refunds get a human.",
      "priority": 700,
      "enabled": true,
      "when": {
        "kind": "condition",
        "id": "c-amount-over-1000",
        "field": "amount",
        "operator": "gt",
        "value": 100000
      },
      "then": {
        "decision": "REQUIRE_APPROVAL",
        "reason": "Refunds above ₹1,000 require a human approver.",
        "approval": { "routingMode": "SINGLE", "requiredApprovals": 1, "ttlSeconds": 900 }
      }
    }
  ]
}
```

This JSON **is** the policy. It is not compiled from a DSL and it is not a cache
of something else — it is the authoritative artefact stored in PostgreSQL, and
it is what the visual Policy Builder reads and writes. Build a policy in the
console and you can read the result; hand-write one and the console renders it.

## Conditions

Two node kinds, composed into a tree.

**A condition** tests one field:

```json
{
  "kind": "condition",
  "id": "c-destructive-op",
  "field": "parameters",
  "path": "operation",
  "operator": "in",
  "value": ["DELETE", "DROP", "TRUNCATE"],
  "ignoreCase": true
}
```

`path` appends a dotted path to a container field, which is how you reach
`parameters.operation` or `resource.attributes.table`. `ignoreCase` applies to
string comparison.

**A group** combines conditions:

```json
{
  "kind": "group",
  "id": "g-prod-deploy",
  "operator": "and",
  "negate": false,
  "children": [
    { "kind": "condition", "id": "c-cap", "field": "capability", "operator": "eq", "value": "deployment.create" },
    { "kind": "condition", "id": "c-env", "field": "environment.type", "operator": "eq", "value": "PRODUCTION" }
  ]
}
```

`operator` is `and` or `or`. `negate` inverts the group's result, which saves
you from needing a `not` operator on every comparison. Groups nest to any depth.

A rule with `"when": null` always matches. That is how you write a catch-all
that is more specific than `defaultDecision`.

Every `id` must be unique within the rule set, and it is not decoration: ids
appear in the per-condition trace on every decision, which is how a condition
that evaluated unexpectedly gets identified.

## Fields and operators

| Operator | For |
| --- | --- |
| `eq` `neq` | Any |
| `gt` `gte` `lt` `lte` `between` | Numbers |
| `in` `not_in` | Membership in a list you supply |
| `contains` `not_contains` | Substring, or list membership |
| `contains_any` `contains_all` | List against list |
| `starts_with` `ends_with` `matches` | Strings; `matches` is a regular expression |
| `exists` `not_exists` | Presence |
| `is_true` `is_false` | Booleans |

The field set is **closed**. An unknown field is a validation error when you
save, not an `undefined` at decision time. All of them, with their types and
permitted operators, are in [Policy fields](/reference/policy-fields) — a
generated page, so it cannot drift.

## Decisions

| Decision | Effect |
| --- | --- |
| `ALLOW` | Proceed |
| `MASK` | Proceed; `maskFields` paths are redacted from the forwarded request and from audit |
| `LIMIT` | Proceed, capped. `limitAmount` and `limitVelocity` apply |
| `STEP_UP_AUTH` | A fresh human authentication is required, no older than `stepUpMaxAgeSeconds` |
| `REQUIRE_APPROVAL` | A human must decide. `approval` controls routing, quorum and TTL |
| `BLOCK` | Refuse |

## Precedence — the part to read twice

Multiple policies can be in scope, and multiple rules can match. The outcome is
fully specified.

**1. Policies are ordered** by priority descending, ties broken by policy key
ascending. Every in-scope policy is evaluated; none short-circuits.

**2. Rules within a policy are ordered** by priority descending, ties broken by
rule id ascending. Disabled rules are skipped. Every rule is still evaluated —
priority orders the trace, it does not stop evaluation.

**3. Every matching rule contributes a candidate decision.** A policy where
*no* rule matched contributes its own `defaultDecision`.

**4. The strictest candidate wins.**

:::diagram precedence | Strictness order, generated from the evaluator's own DECISION_STRICTNESS table.
In descending order of strictness: `BLOCK`, `REQUIRE_APPROVAL`,
`STEP_UP_AUTH`, `LIMIT`, `MASK`, `ALLOW`. Every matching rule contributes a
candidate and the strictest one wins, so a `BLOCK` from a priority-10 rule
beats an `ALLOW` from a priority-900 rule.
:::

:::security Strictest wins, not highest priority
This is the single most important thing on this page. A `BLOCK` from a
priority-10 rule beats an `ALLOW` from a priority-900 rule. Priority orders the
evaluation and the explanation; it does not override strictness.

The consequence is the one you want: you cannot accidentally un-block something
by adding a high-priority allow rule. And a deny-by-default policy bound to a
broad scope will deny everything in that scope regardless of what other
policies say — which is correct, and is also a very easy way to lock yourself
out. Bind narrow.
:::

**5. Effects merge, most restrictive wins.** When several rules contribute
effects, they combine rather than replace:

- `limitAmount` → the minimum
- `limitVelocity` → the minimum of each window
- `maskFields` → the union
- `approval.requiredApprovals` → the maximum
- `approval.ttlSeconds` → the minimum
- `approval.requireStepUp` → true if any rule asks for it
- `stepUpMaxAgeSeconds` → the minimum

So two rules each requiring one approval, with one asking for step-up, produce a
single approval that requires step-up. Nothing is lost in the merge.

## When nothing is bound

If no policy is bound to the request's scope, the evaluator does not invent an
answer. The caller supplies the default, and the gateway's choice is
conservative: `BLOCK` in `ENFORCE` mode for a dangerous capability, `ALLOW`
otherwise. The result carries `noPolicyBound: true`.

`veefore doctor` reports a dangerous tool that is reachable only because of a
default decision. Treat that as a finding, not a configuration.

## Versions

Every save is a new version. Versions are immutable once activated.

```bash
veefore policies list
veefore policies show pol_01ABC
veefore policies diff pol_01ABC --from 3 --to 4
veefore policies test pol_01ABC
veefore policies deploy pol_01ABC --version 4 --reason "Reviewed in change ticket CHG-4411"
```

Activation requires three things together, and none of them is optional:

1. `policy:activate`, which is high-impact and cannot come from a wildcard grant,
2. **step-up authentication**, no matter how fresh the session is,
3. **every recorded test case passing**.

Rollback creates a *new* version with the old content rather than moving a
pointer backwards:

```bash
veefore policies deploy pol_01ABC --version 3 --reason "Rolling back CHG-4411"
```

The history stays linear, which means the audit trail reads as a sequence of
decisions rather than a mutable state you have to reconstruct.

## Testing

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

```bash
veefore policies test
```

Run them locally, in CI, and again automatically at activation. A failing case
blocks activation. There is no override flag, because the one time you want an
override is the one time you should not have one.

## Simulating

```http
POST /v1/policies/simulate
```

Runs the full pipeline against real current state and returns the decision with
its full explanation. No execution, no credential lease, no audit of a decision
that did not happen.

```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", "amount": 150000 }'
```

This is what `veefore deploy` preflight uses to replay your recent observed
traffic and tell you precisely what starts being refused.

## Exporting to Rego

`compileRuleSetToRego` renders an equivalent Rego module, so a team already
running OPA can enforce the same semantics outside the Node process. The export
is for interoperability and review — the TypeScript IR remains the
authoritative artefact. The reasoning is in
[Policy engine](/architecture/adr/0003-policy-engine).

## Related

:::cards

:::card Write your first policy | /build/first-policy
A real policy, built up a rule at a time.
:::

:::card Policy fields | /reference/policy-fields
Every field, type and operator. Generated.
:::

:::card Policy templates | /reference/policy-templates
The built-in starting points, with their full rule sets.
:::

:::card Gate your pipeline | /build/ci-gate
Test cases, simulation and `veefore test` in CI.
:::

:::
