Deterministic Policy Reference
Complete reference for every field, constraint type, evaluation mode, dynamic expression, and session constraint in Veto's deterministic policy engine.
Deterministic policies evaluate tool calls with pure rule logic — no LLM, no latency beyond a single in-process check. A policy defines one or more argument constraints. Each constraint targets one argument of the tool call and specifies what values are allowed, what action to take on violation, and whether to stop at the first failure or collect all failures.
Policy fields
| Field | Type | Default | Description |
|---|---|---|---|
mode | "deterministic" | — | Enables deterministic evaluation |
evaluationMode | "fail_fast" | "collect_all" | "fail_fast" | Whether to stop at first violation or collect all violations |
constraints | ArgumentConstraint[] | [] | Ordered list of argument constraints |
sessionConstraints | SessionConstraints | — | Session-level limits (budget, call counts, counters) |
outputRules | OutputRule[] | [] | Post-execution output validation rules |
Argument constraints
Each entry in constraints targets one argument and specifies one or more checks. All checks on a single constraint entry are AND-ed together. If the argument passes, evaluation moves to the next constraint. If it fails, the constraint's action fires.
Shared fields (all constraint types)
| Field | Type | Default | Description |
|---|---|---|---|
argumentName | string | — | Name of the tool argument to check |
enabled | boolean | — | Set false to skip without deleting |
action | "deny" | "require_approval" | "deny" | What to do when the constraint fails |
Type enforcement
The engine infers the expected type from which constraint fields are present:
| Constraint fields present | Expected type |
|---|---|
minimum, maximum, greaterThan, lessThan, dynamicMinimum, dynamicMaximum | number |
regex, notRegex, enum, notEnum, minLength, maxLength | string |
minItems, maxItems | array |
mustBe | boolean |
Only required / notNull | No type inferred — any type allowed |
If the actual argument type doesn't match the inferred type, the constraint fails immediately with a clear message before any value checks run. For example, passing amount: "99999" (string) against a maximum: 500 constraint is denied with "expected number, got string".
Numeric constraints
Apply when the argument is a number. NaN and Infinity always fail.
| Field | Description | Example |
|---|---|---|
minimum | Value must be ≥ threshold (inclusive) | minimum: 0 |
maximum | Value must be ≤ threshold (inclusive) | maximum: 5000 |
greaterThan | Value must be > threshold (strict) | greaterThan: 0 |
lessThan | Value must be < threshold (strict) | lessThan: 100 |
greaterThanOrEqual | Alias for minimum | greaterThanOrEqual: 1 |
lessThanOrEqual | Alias for maximum | lessThanOrEqual: 999 |
dynamicMinimum | Expression evaluated at runtime (see Dynamic expressions) | "session.remaining * 0.05" |
dynamicMaximum | Expression evaluated at runtime | "session.remaining * 0.15" |
Boundary behaviour: minimum/maximum/greaterThanOrEqual/lessThanOrEqual are inclusive. greaterThan/lessThan are exclusive — the boundary value itself fails.
argument: price
greaterThan: 0 # price=0 → deny, price=0.01 → allow
lessThan: 500 # price=500 → deny, price=499.99 → allowNaN guard: NaN passes all JavaScript comparisons (NaN > 500 is false). The engine explicitly rejects it before any comparison runs.
String constraints
Apply when the argument is a string.
| Field | Description | Example |
|---|---|---|
minLength | String length must be ≥ N | minLength: 1 |
maxLength | String length must be ≤ N | maxLength: 200 |
regex | String must match the pattern | regex: "^[A-Z]{1,5}$" |
notRegex | String must NOT match the pattern | notRegex: "\\.\\." |
enum | String must be one of the allowed values | enum: ["buy", "sell"] |
notEnum | String must NOT be one of the blocked values | notEnum: ["DROP", "TRUNCATE"] |
caseInsensitive | Makes enum and notEnum case-insensitive | caseInsensitive: true |
Regex patterns are capped at 256 characters and validated for ReDoS safety before execution. Invalid or unsafe patterns fail closed (the constraint denies).
Case sensitivity: enum and notEnum are case-sensitive by default. Set caseInsensitive: true to match regardless of casing.
# Allow buy/sell in any casing
argument: side
enum: ["buy", "sell"]
caseInsensitive: true
# BUY, Buy, buy → all allowed# Block destructive SQL regardless of casing
argument: operation
notEnum: ["DROP", "TRUNCATE", "DELETE"]
caseInsensitive: true
# drop, Drop, DROP → all deniedCombining regex and notRegex: Both are checked in order. A value must match regex AND not match notRegex.
# Must start with "ls " but must not reference sensitive paths
argument: command
regex: "^ls "
notRegex: "secret|\.ssh|\.env"
# "ls /tmp" → allow
# "ls /home/user/.ssh" → deny (matches notRegex)Array constraints
Apply when the argument is an array. Only array length is checked — element values are not inspected.
| Field | Description | Example |
|---|---|---|
minItems | Array must have at least N elements | minItems: 1 |
maxItems | Array must have at most N elements | maxItems: 100 |
# Batch operations: must have 1–100 items
argument: user_ids
minItems: 1
maxItems: 100Boolean constraints
Apply when the argument is a boolean.
| Field | Description | Example |
|---|---|---|
mustBe | Argument must equal this exact value | mustBe: true |
Note that mustBe is a boolean equality check, not a truthiness check. confirmed: 1 does not satisfy mustBe: true — the value must literally be true or false.
# Require explicit user confirmation
argument: confirmed
mustBe: true
# confirmed: true → allow
# confirmed: false → deny
# confirmed: 1 → deny (type mismatch, not a boolean)Presence constraints
These run before type and value checks. They apply regardless of the argument's type.
| Field | Description |
|---|---|
required | Argument must be present in the call (not undefined and not null) |
notNull | Argument cannot be null, but may be absent |
required fires on both undefined (missing key) and null (explicit null), with distinct messages:
- Missing:
"Required argument 'X' is missing" - Null:
"Argument 'X' is required and cannot be null"
notNull only fires on explicit null. A missing argument silently skips the constraint.
Falsy-but-present values (0, false, "", []) all pass required — they are considered present.
# to must be provided and cannot be null
argument: to
required: true
# override_reason can be omitted, but cannot be explicitly null
argument: override_reason
notNull: trueEvaluation mode
Set evaluationMode on the policy (not on individual constraints) to control whether evaluation stops at the first failure or continues to collect all failures.
| Mode | Behaviour | Best for |
|---|---|---|
"fail_fast" (default) | Stop at first violation, return immediately | Simple policies, lowest latency |
"collect_all" | Evaluate all constraints, return every violation | Agents that need to fix all errors in one retry |
In collect_all mode:
- All violations are collected into a combined
reasonstring:"amount: value 9999 > 5000; side: 'SHORT' not in [buy, sell]" - If any violation has
action: "deny", the overall decision is"deny"regardless of other violations - If all violations have
action: "require_approval", the overall decision is"require_approval"
Tiered constraints — ordering matters
Multiple constraints can target the same argument. In fail_fast mode (the default), they are evaluated in array order and stop at the first failure. This means order determines which action fires.
A common pattern is a tiered cap: values over a hard limit are denied outright; values over a soft limit require approval.
# CORRECT: hard deny first, soft approval second
constraints:
- argumentName: amount_usd
enabled: true
maximum: 5000
action: deny # > $5000 → hard deny
- argumentName: amount_usd
enabled: true
maximum: 1000
action: require_approval # > $1000 and ≤ $5000 → approval# WRONG ORDER: soft approval fires first for all violations
constraints:
- argumentName: amount_usd
enabled: true
maximum: 1000
action: require_approval # $6000 hits this first → require_approval (wrong!)
- argumentName: amount_usd
enabled: true
maximum: 5000
action: deny # never reached for $6000In fail_fast mode, the wrong order causes values over $5000 to receive require_approval instead of a hard deny. Use collect_all mode if you want the deny to win regardless of constraint order.
Dynamic expressions
dynamicMinimum and dynamicMaximum evaluate an arithmetic expression at call time. The result replaces (or tightens) the static bound.
Expression syntax
| Supported | Examples |
|---|---|
| Arithmetic operators | +, -, *, /, % |
| Parentheses | (session.remaining * 0.1) * 2 |
| Session variables | session.budget, session.spent, session.remaining |
| Named counters | session.counter.<name> |
| Argument values | args.<argumentName> |
| Numeric literals | 0.15, 100, 1000 |
Max expression length: 256 characters.
Session variables
| Variable | Value |
|---|---|
session.budget | Total session budget |
session.spent | Cumulative spend so far in this session |
session.remaining | budget - spent |
session.counter.<name> | Current value of a named counter (0 if not set) |
Argument variables
args.<name> reads a numeric argument from the current tool call. Non-numeric and absent arguments resolve to 0.
# Stop loss must be within 10% of the entry price in the same call
argument: stop_loss
dynamicMinimum: "args.entry_price * 0.90"No session — dynamic constraints are skipped safely
When no sessionId is in the request, session.remaining is Infinity. Expressions like session.remaining * 0.15 evaluate to Infinity, which is not finite, so the dynamic constraint is skipped and the call falls through to any static maximum you've set.
This means a policy with only a dynamicMaximum and no static maximum allows all calls when there is no session — add a static maximum as a fallback if you want a hard cap regardless of session state.
# Safe: static maximum is the fallback when no session exists
argument: amount_usd
maximum: 5000 # hard fallback cap
dynamicMaximum: "session.remaining * 0.15" # tighter when session existsWhen static and dynamic bounds both apply
dynamicMaximum and maximum are combined with Math.min — the stricter of the two wins:
session.remaining = 800
dynamicMaximum = "session.remaining * 0.20" → 160
maximum = 500
effectiveMax = Math.min(500, 160) = 160Similarly, dynamicMinimum and minimum are combined with Math.max.
Fail-closed on bad expressions
| Expression result | Behaviour |
|---|---|
| Valid finite number | Constraint applied normally |
Infinity / -Infinity | Dynamic constraint skipped, static bound used |
NaN (e.g. x / 0) | Constraint denies — fail closed |
| Parse/eval error | Constraint denies — fail closed |
Session constraints
Session constraints track state across multiple tool calls in the same session. They require a sessionId in the request context.
{
"toolName": "place_order",
"arguments": { "amount_usd": 500 },
"context": { "sessionId": "session-abc123" }
}Session constraint checks run before argument constraints. A hard deny from session constraints (budget exceeded, max calls reached) fires immediately.
Session constraint fields
| Field | Type | Description |
|---|---|---|
budget | number | Maximum total spend for the session |
spendArgument | string | Which argument tracks spend against the budget |
maxCalls | number | Maximum number of calls to this tool per session |
cumulativeLimits | CumulativeLimit[] | Per-argument running sum limits |
counters | Record<string, CounterConfig> | Named integer counters (open positions, active connections, etc.) |
Budget
{
"sessionConstraints": {
"budget": 5000,
"spendArgument": "amount_usd"
}
}budget sets the session-wide spend cap. spendArgument names which argument is accumulated as spend. After each allowed call, spent += args[spendArgument].
The session.remaining expression variable equals budget - spent and is available to dynamicMaximum / dynamicMinimum expressions.
Important: budget does nothing without a spend source. Either set spendArgument or define cumulativeLimits — one of these must be present for spent to update.
Cumulative limits
Track the running sum of a numeric argument across calls to the same tool in the same session.
{
"sessionConstraints": {
"cumulativeLimits": [
{ "argumentName": "amount_usd", "maxValue": 10000 }
]
}
}| Call | amount_usd | Running sum | Result |
|---|---|---|---|
| 1 | 3000 | 3000 | allow |
| 2 | 5000 | 8000 | allow |
| 3 | 3000 | 11000 (> 10000) | deny |
| 3 (retry) | 2000 | 10000 (= limit) | allow |
Cumulative limits are per-tool. Two tools each with their own maxValue: 10000 can together accumulate $20,000.
Max calls
Limit how many times a tool can be called per session.
{
"sessionConstraints": {
"maxCalls": 5
}
}maxCalls is scoped to the specific tool this policy applies to. Calls to other tools are not counted.
Named counters
Counters are integers that increment or decrement when specific tools are called. Use them to track open positions, active connections, concurrent tasks, or any other resource that has a lifecycle.
{
"sessionConstraints": {
"counters": {
"open_positions": {
"increment": ["buy_shares"],
"decrement": ["sell_shares"],
"max": 3,
"maxAction": "require_approval"
}
}
}
}| Field | Type | Description |
|---|---|---|
increment | string[] | Tool names that increment this counter |
decrement | string[] | Tool names that decrement this counter |
max | number | Deny or require approval when counter reaches this value |
maxAction | "deny" | "require_approval" | Action when counter hits max (default "deny") |
The counter config must appear on every participating tool's policy. If sell_shares doesn't have the same counter config, decrements never fire — the counter only goes up and eventually locks out all future buys.
# buy_shares policy:
sessionConstraints.counters.open_positions:
increment: [buy_shares]
decrement: [sell_shares]
max: 3
maxAction: require_approval
# sell_shares policy (IDENTICAL counter config required):
sessionConstraints.counters.open_positions:
increment: [buy_shares]
decrement: [sell_shares]
max: 3
maxAction: require_approvalCounters are available as session.counter.<name> in dynamic expressions:
# Limit per-position size based on how many positions are already open
argument: quantity
dynamicMaximum: "session.counter.open_positions * 500"Domain examples
Finance: tiered trade guard
{
"mode": "deterministic",
"constraints": [
{
"argumentName": "symbol",
"enabled": true,
"required": true,
"regex": "^[A-Z]{1,5}$"
},
{
"argumentName": "side",
"enabled": true,
"enum": ["buy", "sell"]
},
{
"argumentName": "quantity",
"enabled": true,
"minimum": 1,
"maximum": 10000
},
{
"argumentName": "amount_usd",
"enabled": true,
"maximum": 5000,
"action": "deny"
},
{
"argumentName": "amount_usd",
"enabled": true,
"maximum": 1000,
"action": "require_approval"
},
{
"argumentName": "order_type",
"enabled": true,
"enum": ["market", "limit", "stop"]
}
],
"sessionConstraints": {
"budget": 25000,
"spendArgument": "amount_usd"
}
}| Scenario | Result |
|---|---|
amount_usd: 500 | allow |
amount_usd: 2500 | require_approval |
amount_usd: 7500 | deny |
symbol: "TOOLONG" | deny (regex) |
order_type: "futures" | deny (enum) |
amount_usd: "500" (string) | deny (type mismatch) |
Email: outbound guard
{
"mode": "deterministic",
"constraints": [
{
"argumentName": "to",
"enabled": true,
"required": true,
"regex": "^[a-zA-Z0-9._%+-]+@company\\.com$"
},
{
"argumentName": "subject",
"enabled": true,
"maxLength": 200
},
{
"argumentName": "body",
"enabled": true,
"maxLength": 10000,
"notRegex": "password|secret|api_key"
},
{
"argumentName": "attachments",
"enabled": true,
"maxItems": 5
}
]
}Database: operation blocklist
{
"mode": "deterministic",
"constraints": [
{
"argumentName": "operation",
"enabled": true,
"notEnum": ["DROP", "TRUNCATE", "DELETE"],
"caseInsensitive": true
}
]
}Filesystem: path traversal prevention
{
"mode": "deterministic",
"constraints": [
{
"argumentName": "path",
"enabled": true,
"required": true,
"notRegex": "\\.\\."
}
]
}Infrastructure: concurrent connection limit
{
"mode": "deterministic",
"sessionConstraints": {
"counters": {
"active_connections": {
"increment": ["open_connection"],
"decrement": ["close_connection"],
"max": 5,
"maxAction": "deny"
}
}
}
}Response shape
A deterministic validation response always includes:
| Field | Type | Description |
|---|---|---|
decision | "allow" | "deny" | "require_approval" | Outcome |
mode | "deterministic" | Always "deterministic" |
reason | string | undefined | Human-readable failure reason |
failedArgument | string | undefined | Name of the first failing argument |
matchedCondition | string | undefined | Which constraint fired (e.g. "maximum: 5000", "type: number") |
validations | ArgumentValidation[] | Per-argument pass/fail list |
latencyMs | number | Time taken to evaluate |
In collect_all mode, reason contains all violations joined with "; ".
When a session is active, the response also includes:
{
"session": {
"budget": 5000,
"spent": 1200,
"remaining": 3800,
"counters": { "open_positions": 2 }
}
}