Skip to main content
Veto sits between your AI agent and tool execution, intercepting every tool call to validate it against your rules before allowing it to run.

Architecture overview

Veto acts as a transparent proxy layer that wraps your tools without changing their interface:
┌───────────┐         ┌────────────┐         ┌──────────────┐
│ AI Agent  │────────▶│    Veto    │────────▶│  Your Tools  │
│  (LLM)    │         │  (Guard)   │         │  (Handlers)  │
└───────────┘         └────────────┘         └──────────────┘

                      ┌─────┴──────┐
                      │ YAML Rules │  block · allow · ask
                      └────────────┘

The interception flow

When you wrap your tools with Veto, it creates a validation layer that intercepts every call:
import { Veto } from 'veto-sdk';

const veto = await Veto.init();
const guarded = veto.wrap(tools);  // Inject guardrails

// Pass guarded tools to your agent
const agent = createAgent({ tools: guarded });
The agent is unaware it’s being governed. Your tools are unchanged. No behavior change for the AI.

Validation process

When the agent calls a tool, Veto follows this validation flow:
1

Tool call intercepted

Veto captures the tool name and arguments before execution
2

Rule matching

Veto finds all rules that apply to this tool
3

Condition evaluation

Deterministic conditions are checked first (local, zero latency)
4

Decision made

Based on the first matching rule:
  • allow → Tool executes normally
  • block → Throws ToolCallDeniedError with reason
  • require_approval → Routes to human approval queue
  • warn → Logs warning, executes tool
  • log → Logs info, executes tool

Rule matching

Veto uses a two-step process to determine which rules apply:
  1. Tool-specific rules — Rules with a matching tools list
  2. Global rules — Rules with no tools field (apply to every call)
rules:
  # Tool-specific: only applies to transfer_funds
  - id: block-large-transfers
    action: block
    tools: [transfer_funds]
    conditions:
      - field: arguments.amount
        operator: greater_than
        value: 10000

  # Global: applies to ALL tools
  - id: log-all-calls
    action: log
    # No tools field = global

Condition evaluation

Conditions are evaluated in order until one matches. Veto prioritizes deterministic conditions for performance:
conditions:
  - field: arguments.amount
    operator: greater_than
    value: 1000
  - field: arguments.currency
    operator: in
    value: ["USD", "EUR", "GBP"]
Static conditions run locally with zero latency and no API calls. They’re perfect for numeric limits, string matching, and structural validation.

Decision flow

Once a rule matches, Veto takes action based on the rule’s action field:

Block

Denies the tool call and throws an error:
try {
  await tools[0].handler({ amount: 15000, recipient: "ACME" });
} catch (error) {
  if (error instanceof ToolCallDeniedError) {
    console.log(error.reason); // "Transfer amount exceeds $10,000 limit"
    console.log(error.validationResult.ruleId); // "block-large-transfers"
  }
}

Allow

Explicitly allows the tool call to proceed. Useful for whitelisting specific patterns:
- id: allow-read-operations
  action: allow
  tools: [get_balance, list_transactions]

Require approval

Routes the call to a human approval workflow. See Human-in-the-loop for details.
- id: require-approval-production
  action: require_approval
  tools: [deploy]
  conditions:
    - field: arguments.environment
      operator: equals
      value: production

Warn and Log

Non-blocking actions that record the event without stopping execution:
  • warn — Logs a warning message (severity: warning)
  • log — Logs an info message (severity: info)
- id: warn-external-api
  action: warn
  conditions:
    - field: arguments.url
      operator: starts_with
      value: "https://external."

History tracking

Veto maintains a call history for each session, enabling:
  • Sequential rules — Block actions based on previous calls
  • Rate limiting — Prevent repeated sensitive operations
  • Audit trails — Export complete decision history

Sequential rules example

- id: block-send-after-secret-read
  name: Block email after reading secrets
  action: block
  tools: [send_email]
  blocked_by:
    - tool: read_file
      within: 3600  # seconds
      conditions:
        - field: arguments.path
          operator: starts_with
          value: "/etc/secrets"
If the agent reads a secret file and then tries to send an email within 1 hour, Veto blocks the email.

Exporting decisions

const veto = await Veto.init();

// After some tool calls...
const json = veto.exportDecisions("json");
const csv = veto.exportDecisions("csv");

console.log(json);
// [
//   {
//     timestamp: "2024-03-04T10:30:00Z",
//     tool_name: "transfer_funds",
//     arguments: { amount: 15000 },
//     decision: "deny",
//     rule_id: "block-large-transfers",
//     reason: "Transfer amount exceeds limit"
//   }
// ]

Performance characteristics

  • Deterministic conditions: ~0.1ms overhead (local evaluation)
  • Expression conditions: ~0.5ms overhead (AST evaluation)
  • No network calls for local validation mode
  • Type preservation: Wrapped tools maintain full TypeScript types
  • Zero runtime dependencies beyond your chosen validation mode

Next steps

Rules

Learn the complete YAML rule format

Validation modes

Choose between local, API, or cloud validation

Human-in-the-loop

Set up approval workflows

Writing rules

Best practices for rule design

Build docs developers (and LLMs) love