Veto/docs

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

FieldTypeDefaultDescription
mode"deterministic"Enables deterministic evaluation
evaluationMode"fail_fast" | "collect_all""fail_fast"Whether to stop at first violation or collect all violations
constraintsArgumentConstraint[][]Ordered list of argument constraints
sessionConstraintsSessionConstraintsSession-level limits (budget, call counts, counters)
outputRulesOutputRule[][]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)

FieldTypeDefaultDescription
argumentNamestringName of the tool argument to check
enabledbooleanSet 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 presentExpected type
minimum, maximum, greaterThan, lessThan, dynamicMinimum, dynamicMaximumnumber
regex, notRegex, enum, notEnum, minLength, maxLengthstring
minItems, maxItemsarray
mustBeboolean
Only required / notNullNo 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.

FieldDescriptionExample
minimumValue must be ≥ threshold (inclusive)minimum: 0
maximumValue must be ≤ threshold (inclusive)maximum: 5000
greaterThanValue must be > threshold (strict)greaterThan: 0
lessThanValue must be < threshold (strict)lessThan: 100
greaterThanOrEqualAlias for minimumgreaterThanOrEqual: 1
lessThanOrEqualAlias for maximumlessThanOrEqual: 999
dynamicMinimumExpression evaluated at runtime (see Dynamic expressions)"session.remaining * 0.05"
dynamicMaximumExpression 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 → allow

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

FieldDescriptionExample
minLengthString length must be ≥ NminLength: 1
maxLengthString length must be ≤ NmaxLength: 200
regexString must match the patternregex: "^[A-Z]{1,5}$"
notRegexString must NOT match the patternnotRegex: "\\.\\."
enumString must be one of the allowed valuesenum: ["buy", "sell"]
notEnumString must NOT be one of the blocked valuesnotEnum: ["DROP", "TRUNCATE"]
caseInsensitiveMakes enum and notEnum case-insensitivecaseInsensitive: 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 denied

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

FieldDescriptionExample
minItemsArray must have at least N elementsminItems: 1
maxItemsArray must have at most N elementsmaxItems: 100
# Batch operations: must have 1–100 items
argument: user_ids
minItems: 1
maxItems: 100

Boolean constraints

Apply when the argument is a boolean.

FieldDescriptionExample
mustBeArgument must equal this exact valuemustBe: 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.

FieldDescription
requiredArgument must be present in the call (not undefined and not null)
notNullArgument 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: true

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

ModeBehaviourBest for
"fail_fast" (default)Stop at first violation, return immediatelySimple policies, lowest latency
"collect_all"Evaluate all constraints, return every violationAgents that need to fix all errors in one retry

In collect_all mode:

  • All violations are collected into a combined reason string: "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 $6000

In 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

SupportedExamples
Arithmetic operators+, -, *, /, %
Parentheses(session.remaining * 0.1) * 2
Session variablessession.budget, session.spent, session.remaining
Named counterssession.counter.<name>
Argument valuesargs.<argumentName>
Numeric literals0.15, 100, 1000

Max expression length: 256 characters.

Session variables

VariableValue
session.budgetTotal session budget
session.spentCumulative spend so far in this session
session.remainingbudget - 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 exists

When 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) = 160

Similarly, dynamicMinimum and minimum are combined with Math.max.

Fail-closed on bad expressions

Expression resultBehaviour
Valid finite numberConstraint applied normally
Infinity / -InfinityDynamic constraint skipped, static bound used
NaN (e.g. x / 0)Constraint denies — fail closed
Parse/eval errorConstraint 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

FieldTypeDescription
budgetnumberMaximum total spend for the session
spendArgumentstringWhich argument tracks spend against the budget
maxCallsnumberMaximum number of calls to this tool per session
cumulativeLimitsCumulativeLimit[]Per-argument running sum limits
countersRecord<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 }
    ]
  }
}
Callamount_usdRunning sumResult
130003000allow
250008000allow
3300011000 (> 10000)deny
3 (retry)200010000 (= 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"
      }
    }
  }
}
FieldTypeDescription
incrementstring[]Tool names that increment this counter
decrementstring[]Tool names that decrement this counter
maxnumberDeny 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_approval

Counters 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"
  }
}
ScenarioResult
amount_usd: 500allow
amount_usd: 2500require_approval
amount_usd: 7500deny
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:

FieldTypeDescription
decision"allow" | "deny" | "require_approval"Outcome
mode"deterministic"Always "deterministic"
reasonstring | undefinedHuman-readable failure reason
failedArgumentstring | undefinedName of the first failing argument
matchedConditionstring | undefinedWhich constraint fired (e.g. "maximum: 5000", "type: number")
validationsArgumentValidation[]Per-argument pass/fail list
latencyMsnumberTime 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 }
  }
}