Veefore docs

Policy

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 — 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.

Decision precedence: strictest winsHorizontal bars for each policy decision, longest for BLOCK and shortest for ALLOW, showing the strictness order.Strictest candidate winsPriority orders evaluation. It does not override strictness.BLOCKstrictness 5REQUIRE_APPROVALstrictness 4STEP_UP_AUTHstrictness 3LIMITstrictness 2MASKstrictness 1ALLOWstrictness 0A BLOCK from a priority-10 rule beats an ALLOW from a priority-900 rule.
Strictness order, generated from the evaluator's own DECISION_STRICTNESS table.
Described in text

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.

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.

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