Veto/docs

Local Rule Evaluation

Evaluate Veto rules locally without cloud round-trips.

evaluateRulesLocally evaluates an array of rules against a tool call entirely client-side. Sub-millisecond, no network, no API key needed. Useful for browser extensions, offline scenarios, or pre-filtering before a cloud call.

Import

import { evaluateRulesLocally } from 'veto-sdk';
// or target the subpath directly
import { evaluateRulesLocally } from 'veto-sdk/rules';

API

function evaluateRulesLocally(
  rules: Rule[],
  toolName: string,
  args: Record<string, unknown>,
): LocalEvalResult;

interface LocalEvalResult {
  decision: 'allow' | 'deny' | 'require_approval' | null;
  reason?: string;   // rule description or name
  ruleId?: string;   // ID of the matching rule
}

decision: null means no rule matched — fall through to cloud evaluation or allow by default.

import { evaluateRulesLocally } from 'veto-sdk/rules';

const result = evaluateRulesLocally(rules, 'browser_inputText', {
  arguments: {
    text: 'VISA 4111 1111 1111 1111',
    extracted_entities: { has_credit_cards: true },
  },
});

if (result.decision === 'deny') {
  throw new Error(`Blocked by rule ${result.ruleId}: ${result.reason}`);
}
if (result.decision === 'require_approval') {
  await requestHumanApproval(result.reason);
}
if (result.decision === null) {
  // No local rule matched — proceed or call cloud
}

Operators

All 16 operators supported:

OperatorField typeValue typeNotes
equalsanyanyCase-insensitive for strings
not_equalsanyanyCase-insensitive for strings
containsstring or arraystringCase-insensitive for strings
not_containsstring or arraystringCase-insensitive for strings
starts_withstringstringCase-insensitive
ends_withstringstringCase-insensitive
matchesstringregex stringCase-insensitive flag applied automatically
greater_thannumbernumber
less_thannumbernumber
inanyarrayField value must be in the array
not_inanyarrayField value must not be in the array
length_greater_thanstring or arraynumberChecks .length
percent_ofnumbernumberSee divergences below
within_hours"HH:MM-HH:MM"Checks wall clock time, not field value
outside_hours"HH:MM-HH:MM"Inverse of within_hours
// Block navigation to competitor domains
{
  field: 'arguments.url',
  operator: 'contains',
  value: 'competitor.com'   // case-insensitive
}

// Block purchases over $200
{
  field: 'arguments.extracted_entities.max_price',
  operator: 'greater_than',
  value: 200
}

// Block actions outside business hours (wall clock, 24h format)
{
  field: 'arguments.current_url',   // field is evaluated but ignored for time check
  operator: 'outside_hours',
  value: '09:00-17:00'
}

// Block when a list has more than 5 items
{
  field: 'arguments.extracted_entities.prices',
  operator: 'length_greater_than',
  value: 5
}

// Block when tool is one of a set
{
  field: 'arguments.action_type',
  operator: 'in',
  value: ['submit', 'checkout', 'confirm']
}

Evaluation order

  1. Disabled rules (enabled: false) are skipped.
  2. Rules with a tools array only match when toolName is in that array. Rules without tools match all tools.
  3. Rules are evaluated in array order. First matching rule wins — evaluation stops immediately.
  4. If a rule has conditions (non-empty), all conditions must match (AND). condition_groups is ignored.
  5. If conditions is absent or empty, condition_groups is evaluated: any group matching wins (OR between groups, AND within each group).
  6. Rules with action warn or log are skipped for enforcement — they do not produce a decision and evaluation continues to the next rule.
// This rule only fires for browser_inputText
{
  id: 'block-ssn-input',
  enabled: true,
  action: 'block',
  tools: ['browser_inputText'],
  conditions: [
    { field: 'arguments.text', operator: 'matches', value: '\\b\\d{3}-\\d{2}-\\d{4}\\b' }
  ]
}

// This rule fires for any tool (no tools filter)
{
  id: 'block-credit-cards-anywhere',
  enabled: true,
  action: 'block',
  conditions: [
    { field: 'arguments.extracted_entities.has_credit_cards', operator: 'equals', value: true }
  ]
}

// condition_groups: OR between groups, AND within each group
{
  id: 'block-risky-pages',
  enabled: true,
  action: 'require_approval',
  condition_groups: [
    [{ field: 'arguments.current_url', operator: 'contains', value: 'checkout' }],
    [{ field: 'arguments.current_url', operator: 'contains', value: 'payment' }],
  ]
}

Key behaviors

Undefined fields never match. If a field path resolves to undefined, the condition is false regardless of operator. This prevents false positives on negative operators (not_equals, not_contains, not_in) when data is simply absent.

// If extracted_entities is not present in args, this does NOT match
{ field: 'arguments.extracted_entities.has_credit_cards', operator: 'equals', value: true }

// And critically, this also does NOT match (undefined is not "not equal to anything")
{ field: 'arguments.extracted_entities.has_credit_cards', operator: 'not_equals', value: false }

warn and log actions are non-enforcing. A rule with action: 'warn' or action: 'log' will not produce a decision. Evaluation continues past it to the next rule.

Field paths use dot notation. Deep paths like arguments.element_context.row_text are resolved by walking the object — any broken segment returns undefined.

Divergences from cloud evaluator

The local evaluator intentionally diverges from the canonical cloud evaluator in three ways:

String case sensitivity. The local evaluator applies case-insensitive comparison for all string operators (equals, not_equals, contains, not_contains, starts_with, ends_with, matches). The cloud evaluator is case-sensitive. If your rules rely on exact case matching, cloud evaluation will behave differently.

within_hours / outside_hours use wall clock time. The local evaluator checks the current wall clock time against a simple "HH:MM-HH:MM" string. It does not use the field value, does not support timezones, and does not support day-of-week filtering. The cloud evaluator interprets the field value as a timestamp and uses structured time window objects with timezone and day-of-week support. Wrap-around overnight ranges (e.g. "22:00-06:00") work correctly in both.

percent_of with no reference field. When no reference field is provided on the condition, the local evaluator falls back to fieldValue >= expected. The cloud evaluator returns false without a reference field.

Full pipeline example

import { evaluateRulesLocally } from 'veto-sdk/rules';
import { tryInstantGeneration, validatePolicyOutput, BROWSER_AGENT_SYSTEM_PROMPT } from 'veto-sdk/policy';

// 1. Generate rules from natural language
const instant = tryInstantGeneration('block purchases over $100');
const rules = instant?.rules ?? (await generateWithLLM('block purchases over $100')).rules;

// 2. Evaluate locally before each action
function beforeAction(toolName: string, toolArgs: Record<string, unknown>) {
  const context = {
    arguments: {
      ...toolArgs,
      extracted_entities: extractEntities(toolArgs), // your entity extractor
    },
  };

  const result = evaluateRulesLocally(rules, toolName, context);

  if (result.decision === 'deny') {
    throw new Error(`Action blocked: ${result.reason}`);
  }
  if (result.decision === 'require_approval') {
    return waitForApproval(result.reason);
  }
  // decision === null: no local rule matched, proceed
}