Skip to main content

A guardrail system that intercepts and validates AI agent tool calls

Project description

veto

PyPI License

A guardrail system for AI agent tool calls. Veto intercepts and validates tool calls made by AI models before execution -- blocking, allowing, or routing to human approval.

How it works

  1. Initialize Veto (loads your YAML rules).
  2. Wrap your tools with veto.wrap().
  3. Pass the wrapped tools to your agent -- interface unchanged.

When the AI calls a tool, Veto automatically:

  1. Intercepts the call.
  2. Validates arguments against your rules (deterministic conditions first, optional LLM for semantic rules).
  3. allow -- executes. block -- denied with reason. ask -- routed to approval queue.

The agent is unaware of the guardrail.

Installation

pip install veto

With LLM provider support:

pip install veto[openai]      # OpenAI
pip install veto[anthropic]   # Anthropic
pip install veto[gemini]      # Google Gemini
pip install veto[all]         # All providers

For a complete human-in-the-loop example, see the HITL guide.

Quick start

1. Initialize Veto

veto init

Creates ./veto/veto.config.yaml and default rules.

2. Wrap your tools

from veto import Veto

my_tools = [
    {"name": "my_tool", "handler": my_handler},
]

veto = await Veto.init()
wrapped_tools = veto.wrap(my_tools)

agent = create_agent(tools=wrapped_tools)

3. Configure rules

Edit veto/rules/financial.yaml:

rules:
  - id: limit-transfers
    name: Limit large transfers
    action: block
    tools:
      - transfer_funds
    conditions:
      - field: arguments.amount
        operator: greater_than
        value: 1000

Configuration

veto.config.yaml

version: "1.0"

mode: "strict" # "strict" blocks calls, "log" only logs them

validation:
  mode: "custom" # "api" or "custom"

custom:
  provider: "gemini" # openai | anthropic | gemini
  model: "gemini-3-flash-preview"

logging:
  level: "info"

rules:
  directory: "./rules"
  recursive: true

API Reference

Veto.init(options?)

Initialize Veto. Loads configuration from ./veto by default.

veto = await Veto.init()

veto.wrap(tools)

Wrap a list of tools. Injects Veto validation into each tool's execution handler.

wrapped_tools = veto.wrap(my_tools)

veto.wrap_tool(tool)

Wrap a single tool.

safe_tool = veto.wrap_tool(my_tool)

veto.get_history_stats()

Statistics on allowed vs blocked calls.

stats = veto.get_history_stats()
# {"total_calls": 5, "allowed_calls": 4, "denied_calls": 1, ...}

veto.clear_history()

Reset history statistics.

veto.export_decisions(format)

Export decision history as JSON or CSV.

json_audit = veto.export_decisions("json")
csv_audit  = veto.export_decisions("csv")

Rate Limiting

Per-rule sliding window rate limits. In-memory store by default; bring your own store (e.g. Redis) by implementing the RateLimitStore protocol.

Rule configuration

rules:
  - id: throttle-emails
    name: Throttle email sending
    action: block
    tools:
      - send_email
    rate_limits:
      - scope: user # agent | user | session | global
        window_seconds: 60
        max_calls: 10

Programmatic use

from veto import evaluate_rate_limits, RateLimitEntry, RateLimitStore

limits = [RateLimitEntry(scope="global", max_calls=5, window_seconds=60)]

# ctx must have agent_id/user_id/session_id attributes matching the scope
reason = await evaluate_rate_limits(limits, ctx, tool_name="send_email", logger=logger)
if reason:
    print(f"Blocked: {reason}")

Custom store

Implement the RateLimitStore protocol to back rate limits with Redis or another external store:

from veto import RateLimitStore

class RedisRateLimitStore:
    def check_and_record(self, key: str, max_calls: int, window_ms: int) -> bool:
        # Return True if allowed, False if rate limited
        ...

    def clear(self) -> None:
        ...

reason = await evaluate_rate_limits(limits, ctx, "send_email", logger, store=my_store)

Built-in store functions

from veto import check_and_record, clear_store

allowed = check_and_record("my-key", max_calls=10, window_ms=60000)
clear_store()  # reset all rate limit state

Audit Chain

SHA-256 hash chain for tamper-evident decision logging. Each hash is computed over the previous hash concatenated with a deterministic JSON serialization of the record.

from veto import compute_chain_hash, GENESIS_HASH

chain_hash = GENESIS_HASH  # empty string

for decision in decisions:
    chain_hash = compute_chain_hash(chain_hash, decision)
    store(decision, chain_hash)

# To verify: recompute the chain from genesis and compare hashes.
# Any mutation to a historical record invalidates all subsequent hashes.

compute_chain_hash(prev_hash: str, record: Any) -> str -- returns a hex-encoded SHA-256 digest.

GENESIS_HASH -- empty string (""), the starting point of every chain.

OpenTelemetry

Optional integration. If opentelemetry-api is installed, try_load_otel() returns a real tracer. Otherwise it returns a no-op tracer -- zero cost, no import errors.

from veto import try_load_otel, SPAN_STATUS_OK, SPAN_STATUS_ERROR

tracer = try_load_otel(service_name="my-agent")

span = tracer.start_span("veto.validate")
span.set_attribute("tool.name", "transfer_funds")
span.set_status(SPAN_STATUS_OK)
span.end()

Types

  • VetoTracer -- protocol with start_span(name: str) -> VetoSpan
  • VetoSpan -- protocol with set_attribute(), set_status(), end()
  • SPAN_STATUS_OK = 1, SPAN_STATUS_ERROR = 2

Policy Testing

YAML fixture-based policy testing. No LLM, no network. Evaluates test cases against your rule files using the same condition logic as the runtime.

Write fixtures

veto/tests/financial.yaml:

suite: Financial rules
tests:
  - id: block-large-transfer
    tool: transfer_funds
    arguments:
      amount: 5000
    expect:
      decision: block
      rule_id: limit-transfers

  - id: allow-small-transfer
    tool: transfer_funds
    arguments:
      amount: 50
    expect:
      decision: allow

Run tests

from veto import run_tests

result = run_tests(
    fixtures_path="./veto/tests",
    policy_path="./veto",
    coverage=True,  # print rule coverage report
    quiet=False,    # print pass/fail per test
)

print(f"{result.passed}/{result.total} passed, {result.failed} failed")

for r in result.results:
    if not r.passed:
        print(f"  FAIL {r.test_id}: {r.error}")

Types

  • VetoTestRunResult -- total, passed, failed, results: list[VetoTestResult]
  • VetoTestResult -- test_id, suite, passed, expected, actual_decision, actual_rule_id, error
  • VetoTestSuite -- suite name + tests: list[VetoTestCase]
  • VetoTestCase -- id, tool, arguments, expect, optional description and context

Supported condition operators

equals, not_equals, contains, greater_than, less_than, in, not_in, exists, not_exists

Cloud Client

Register tools and validate calls against cloud-managed policies via the Veto Cloud API.

from veto import VetoCloudClient, VetoCloudConfig

config = VetoCloudConfig(
    api_key="veto_sk_...",      # or set VETO_API_KEY env var
    base_url="https://api.veto.so",  # default
    timeout=30000,              # ms
    retries=2,
)
client = VetoCloudClient(config)

Register tools

from veto import ToolRegistration, ToolParameter

tools = [
    ToolRegistration(
        name="transfer_funds",
        description="Transfer money between accounts",
        parameters=[
            ToolParameter(name="amount", type="number", required=True),
            ToolParameter(name="to_account", type="string", required=True),
        ],
    )
]
response = await client.register_tools(tools)

Validate a tool call

result = await client.validate(
    tool_name="transfer_funds",
    arguments={"amount": 500, "to_account": "acct_123"},
    context={"user_id": "usr_456"},
)
# result.decision: "allow" | "deny" | "require_approval"
# result.reason: str | None
# result.failed_constraints: list[FailedConstraint]

Poll for human approval

from veto import ApprovalTimeoutError

if result.decision == "require_approval" and result.approval_id:
    try:
        approval = await client.poll_approval(result.approval_id)
        # approval.status: "approved" | "denied"
    except ApprovalTimeoutError:
        print("Approval timed out")

Policy cache

Stale-while-revalidate cache for cloud policies. Background refresh keeps latency low.

from veto import PolicyCache

cache = PolicyCache(client, fresh_seconds=60, max_seconds=300)
policy = cache.get("transfer_funds")  # returns DeterministicPolicy or None
cache.invalidate("transfer_funds")
cache.invalidate_all()

Cleanup

await client.close()

Provider Adapters

Convert between Veto's internal tool format and provider-specific formats. Adapters exist for OpenAI, Anthropic, and Google (Gemini).

OpenAI

from veto import to_openai, to_openai_tools, from_openai, from_openai_tool_call
from veto import ToolDefinition

tool = ToolDefinition(
    name="get_weather",
    description="Get current weather",
    input_schema={"type": "object", "properties": {"city": {"type": "string"}}},
)

openai_tool = to_openai(tool)               # single tool
openai_tools = to_openai_tools([tool])      # batch

veto_tool = from_openai(openai_tool)        # convert back
veto_call = from_openai_tool_call(tc)       # parse tool call from response

Anthropic

from veto import to_anthropic, to_anthropic_tools, from_anthropic, from_anthropic_tool_use

anthropic_tool = to_anthropic(tool)
anthropic_tools = to_anthropic_tools([tool])

veto_tool = from_anthropic(anthropic_tool)
veto_call = from_anthropic_tool_use(tool_use_block)

Google (Gemini)

from veto import to_google_tool, from_google_function_call

google_tool = to_google_tool([tool1, tool2])   # wraps all declarations in one object
veto_call = from_google_function_call(fc)      # parse function call from response

Output Patterns

Reference regex patterns for detecting sensitive data in tool outputs. These are not applied automatically -- use them in your own output validation or redaction logic.

from veto import (
    OUTPUT_PATTERNS,
    OUTPUT_PATTERN_SSN,
    OUTPUT_PATTERN_CREDIT_CARD,
    OUTPUT_PATTERN_EMAIL,
    OUTPUT_PATTERN_US_PHONE,
    OUTPUT_PATTERN_OPENAI_API_KEY,
    OUTPUT_PATTERN_GITHUB_API_KEY,
    OUTPUT_PATTERN_AWS_API_KEY,
)

import re

text = "Call me at 555-123-4567"
if re.search(OUTPUT_PATTERN_US_PHONE, text):
    print("Phone number detected")

# OUTPUT_PATTERNS is a dict mapping names to patterns:
# {"ssn": r"...", "credit_card": r"...", "email": r"...", ...}
for name, pattern in OUTPUT_PATTERNS.items():
    if re.search(pattern, text):
        print(f"Matched: {name}")

Webhooks

Format decision events for external notification systems. Four built-in formatters: Slack, PagerDuty, generic JSON, and CEF (Common Event Format).

Configuration (YAML)

events:
  webhook:
    url: "https://hooks.slack.com/services/T00/B00/xxx"
    on: [deny, require_approval, budget_exceeded]
    min_severity: medium # critical | high | medium | low | info
    format: slack # slack | pagerduty | generic | cef

Formatting payloads manually

from veto import (
    WebhookEvent,
    format_slack_payload,
    format_pagerduty_payload,
    format_generic_payload,
    format_cef_payload,
)

event = WebhookEvent(
    event_type="deny",
    tool_name="transfer_funds",
    arguments={"amount": 50000},
    decision="deny",
    reason="Amount exceeds limit",
    rule_id="limit-transfers",
    severity="high",
    timestamp="2026-04-06T12:00:00Z",
)

slack_body = format_slack_payload(event)         # Slack Block Kit
pd_body = format_pagerduty_payload(event)        # PagerDuty Events API v2
generic_body = format_generic_payload(event)     # flat JSON dict
cef_line = format_cef_payload(event)             # CEF:0|Veto|SDK|... string

Types

  • WebhookEventType -- "deny" | "require_approval" | "budget_exceeded"
  • WebhookFormat -- "slack" | "pagerduty" | "generic" | "cef"
  • EventWebhookConfig -- url, on, min_severity, format

Deterministic Validation

Validate tool call arguments against constraints without an LLM. Supports numeric bounds, string length/regex/enum, array size, required/not-null checks, and ReDoS-safe regex validation.

from veto import validate_deterministic, ArgumentConstraint

constraints = [
    ArgumentConstraint(
        argument_name="amount",
        minimum=0,
        maximum=10000,
    ),
    ArgumentConstraint(
        argument_name="currency",
        enum=["USD", "EUR", "GBP"],
    ),
    ArgumentConstraint(
        argument_name="recipient",
        required=True,
        min_length=1,
        max_length=100,
        regex=r"^[a-zA-Z0-9_]+$",
    ),
]

result = validate_deterministic(
    tool_name="transfer_funds",
    args={"amount": 500, "currency": "USD", "recipient": "alice"},
    constraints=constraints,
)

# result.decision: "allow" | "deny"
# result.reason: str | None
# result.failed_argument: str | None
# result.validations: list[ValidationEntry]
# result.latency_ms: float

Regex safety

is_safe_pattern(pattern: str) -> bool checks for ReDoS-vulnerable patterns before compilation. The validator uses this internally; you can call it directly.

from veto import is_safe_pattern

is_safe_pattern(r"^[a-z]+$")          # True
is_safe_pattern(r"(a+)+$")            # False -- catastrophic backtracking

Types

  • ArgumentConstraint -- all constraint fields (minimum, maximum, greater_than, less_than, regex, enum, min_length, max_length, min_items, max_items, required, not_null)
  • DeterministicPolicy -- tool_name, mode, constraints, version
  • LocalValidationResult -- decision, reason, failed_argument, validations, latency_ms

Policy Validation

Validate policy YAML/JSON documents against the Policy IR v1 schema. Catches structural errors before runtime.

from veto import validate_policy_ir, PolicySchemaError

policy_doc = {
    "version": 1,
    "rules": [
        {
            "id": "limit-transfers",
            "action": "block",
            "tools": ["transfer_funds"],
            "conditions": [
                {"field": "arguments.amount", "operator": "greater_than", "value": 1000}
            ],
        }
    ],
}

try:
    validate_policy_ir(policy_doc)
except PolicySchemaError as e:
    for err in e.errors:
        print(f"{err.path}: {err.message}")

PolicySchemaError.errors is a list of PolicyValidationError(path, message, keyword).

Rule YAML Reference

Same schema as the TypeScript SDK. See full rule reference.

rules:
  - id: unique-rule-id
    name: Human readable name
    action: block # block | warn | log | allow | ask
    tools: [make_payment]
    conditions:
      - field: arguments.amount
        operator: greater_than
        value: 1000
    description: "Semantic description for LLM validation (optional)."
    rate_limits:
      - scope: global
        window_seconds: 60
        max_calls: 10

License

Apache-2.0 (c) Plaw, Inc.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

veto-0.14.1.tar.gz (176.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

veto-0.14.1-py3-none-any.whl (140.4 kB view details)

Uploaded Python 3

File details

Details for the file veto-0.14.1.tar.gz.

File metadata

  • Download URL: veto-0.14.1.tar.gz
  • Upload date:
  • Size: 176.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for veto-0.14.1.tar.gz
Algorithm Hash digest
SHA256 720a230bea6c321a886461bd09b81cca555dfed579d25e47ae11329834801604
MD5 cfc4ef571ba2e193f3c02d69141cf579
BLAKE2b-256 fda52b0052947ae284633aceab4c11827aeca521651f3645ef9590fdd1357724

See more details on using hashes here.

File details

Details for the file veto-0.14.1-py3-none-any.whl.

File metadata

  • Download URL: veto-0.14.1-py3-none-any.whl
  • Upload date:
  • Size: 140.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for veto-0.14.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8f9ef8817c0b0c6850ba06cfcd6f308b3501dec80b3d3f2449a868047616e18f
MD5 a7828b042052baa8d4892f55ae5b68a0
BLAKE2b-256 b910df9d243f6f59953c115d601407fa98ceedeb8e6d60a8604a5cd8528a70ce

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page