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.
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 minimumlimitVelocity→ the minimum of each windowmaskFields→ the unionapproval.requiredApprovals→ the maximumapproval.ttlSeconds→ the minimumapproval.requireStepUp→ true if any rule asks for itstepUpMaxAgeSeconds→ 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.
bashveefore 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:
policy:activate, which is high-impact and cannot come from a wildcard grant,- step-up authentication, no matter how fresh the session is,
- every recorded test case passing.
Rollback creates a new version with the old content rather than moving a pointer backwards:
bashveefore 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.
bashveefore 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
httpPOST /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.
bashcurl -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.