Framework

PydanticAI Agent Guardrails with Veto

Type-safe agents with validation-based authorization. PydanticAI's model-agnostic framework combined with Veto's runtime policy enforcement gives you agents that respect boundaries by design.

PydanticAI guardrailsPydanticAI authorizationPydanticAI security

Why PydanticAI + Veto

PydanticAI brings type safety to AI agents through Pydantic models. Veto extends that safety to the runtime layer, ensuring every tool call passes through explicit authorization before execution. Together, they give you agents that are both type-safe and permission-aware.

The integration is transparent to your agent. Wrap your tools with Veto'sprotect() function, define policies in YAML, and every tool invocation gets validated against your rules.

Installation

pip install veto pydantic-ai

Quick start

Wrap your PydanticAI agent's tools with Veto's protection layer. The agent remains unaware of the authorization checks happening behind the scenes.

agent.py
from pydantic_ai import Agent, RunContext
from pydantic import BaseModel
from veto import protect
from typing import Literal

class ToolArgs(BaseModel):
    recipient: str
    amount: float
    currency: Literal["USD", "EUR", "GBP"]

# Define your tools with Pydantic validation
async def transfer_funds(ctx: RunContext, args: ToolArgs) -> str:
    """Transfer funds to a recipient."""
    # Your transfer logic here
    return f"Transferred {args.amount} {args.currency} to {args.recipient}"

# Wrap with Veto protection
protected_transfer = protect(transfer_funds, policy="transfers")

# Create the agent with protected tools
agent = Agent(
    "openai:gpt-4o",
    tools=[protected_transfer],
    system_prompt="You are a financial assistant."
)

# Run with automatic authorization
result = await agent.run("Send $500 to alice@example.com")
print(result.data)

Type-safe policy patterns

Veto policies work directly with your Pydantic models. Define rules that inspect validated arguments, enforce domain constraints, and require approvals for sensitive operations.

veto/rules/transfers.yaml
version: "1.0"
name: Transfer authorization rules

rules:
  - id: block-unverified-recipients
    name: Block transfers to unverified recipients
    action: block
    tools: [transfer_funds]
    conditions:
      - field: arguments.recipient
        operator: not_in
        value: "verified_recipients.list"

  - id: limit-transfer-amount
    name: Enforce daily transfer limits
    action: block
    tools: [transfer_funds]
    conditions:
      - field: arguments.amount
        operator: greater_than
        value: 10000
      - field: user.tier
        operator: equals
        value: "standard"

  - id: review-large-transfers
    name: Require approval for large transfers
    action: require_approval
    tools: [transfer_funds]
    conditions:
      - field: arguments.amount
        operator: greater_than
        value: 5000
    approval:
      timeout_minutes: 30
      notify: ["finance@company.com"]

  - id: restrict-currency
    name: Restrict currency by region
    action: block
    tools: [transfer_funds]
    conditions:
      - field: arguments.currency
        operator: equals
        value: "GBP"
      - field: user.region
        operator: not_equals
        value: "UK"

Using with dependencies

Pass user context and dependencies through PydanticAI's dependency injection. Veto uses this context to evaluate policies with user-specific constraints.

agent_with_deps.py
from pydantic_ai import Agent, RunContext
from pydantic import BaseModel
from veto import protect, VetoContext

class UserDeps(BaseModel):
    user_id: str
    tier: str
    region: str
    verified_recipients: list[str]

class TransferArgs(BaseModel):
    recipient: str
    amount: float
    currency: str

async def transfer_funds(
    ctx: RunContext[UserDeps],
    args: TransferArgs
) -> str:
    # Access dependencies via ctx.deps
    return f"Transferred {args.amount} to {args.recipient}"

# Inject context into Veto
protected_transfer = protect(
    transfer_funds,
    policy="transfers",
    context_fn=lambda ctx: VetoContext(
        user_id=ctx.deps.user_id,
        user={"tier": ctx.deps.tier, "region": ctx.deps.region},
        metadata={"verified_recipients": ctx.deps.verified_recipients}
    )
)

agent = Agent("openai:gpt-4o", tools=[protected_transfer])

# Run with user context
user_deps = UserDeps(
    user_id="user_123",
    tier="premium",
    region="US",
    verified_recipients=["alice@example.com", "bob@example.com"]
)

result = await agent.run(
    "Send $200 to alice@example.com",
    deps=user_deps
)

FAQ

Does Veto work with PydanticAI's streaming mode?
Yes. Veto intercepts tool calls before execution, independent of how the response is delivered. Streaming responses work normally while authorization checks happen synchronously before any tool runs.
Can I use Veto with multiple LLM providers in PydanticAI?
Absolutely. PydanticAI is model-agnostic and so is Veto. Whether you're using OpenAI, Anthropic, Gemini, or local models, the authorization layer works identically. Switch models without changing your policies.
How do type validations interact with authorization?
Pydantic validates arguments first. If validation fails, the tool call errors before Veto sees it. If validation passes, Veto receives properly typed arguments to evaluate against your policies. This ordering ensures policies always work with well-formed data.
What happens when a tool call is blocked?
Veto returns a structured rejection that your agent receives as a tool response. The agent can then inform the user, request alternative actions, or escalate. For require_approval actions, the tool pauses until a human approves or denies the request through the Veto dashboard or API.

Related integrations

Ready to add guardrails to your PydanticAI agents?