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:
| Operator | Field type | Value type | Notes |
|---|---|---|---|
equals | any | any | Case-insensitive for strings |
not_equals | any | any | Case-insensitive for strings |
contains | string or array | string | Case-insensitive for strings |
not_contains | string or array | string | Case-insensitive for strings |
starts_with | string | string | Case-insensitive |
ends_with | string | string | Case-insensitive |
matches | string | regex string | Case-insensitive flag applied automatically |
greater_than | number | number | |
less_than | number | number | |
in | any | array | Field value must be in the array |
not_in | any | array | Field value must not be in the array |
length_greater_than | string or array | number | Checks .length |
percent_of | number | number | See 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
- Disabled rules (
enabled: false) are skipped. - Rules with a
toolsarray only match whentoolNameis in that array. Rules withouttoolsmatch all tools. - Rules are evaluated in array order. First matching rule wins — evaluation stops immediately.
- If a rule has
conditions(non-empty), all conditions must match (AND).condition_groupsis ignored. - If
conditionsis absent or empty,condition_groupsis evaluated: any group matching wins (OR between groups, AND within each group). - Rules with action
warnorlogare 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
}