Veto Python SDK
Native Python SDK for AI agent authorization. Async-native, framework-agnostic, and works with LangChain, CrewAI, PydanticAI, AutoGen, FastAPI, and Django. Two lines of code to add runtime guardrails.
Installation
pip install veto
Requires Python 3.9+. The SDK is async-native but provides sync wrappers for Django and other synchronous frameworks.
Quick start
Initialize Veto, wrap your tools, and call them normally. Blocked calls raise ToolCallDeniedError with the policy reason attached.
import asyncio
from veto import Veto, ToolCallDeniedError
async def transfer_money(amount: float, to_account: str) -> str:
return f"Transferred $" + str(amount) + f" to {to_account}"
async def delete_file(path: str) -> str:
return f"Deleted {path}"
async def main():
veto = await Veto.init() # reads VETO_API_KEY from env
wrapped = veto.wrap([transfer_money, delete_file])
try:
result = await wrapped[0].handler({"amount": 500, "to_account": "ACC123"})
print(f"Allowed: {result}")
except ToolCallDeniedError as e:
print(f"Blocked: {e.reason}")
asyncio.run(main())Shorthand with protect()
The protect() function combines initialization and wrapping into a single call. Best for quick setup and scripts.
from veto import protect # One-line wrapping. Reads VETO_API_KEY from env. safe_tools = await protect([transfer_money, delete_file]) # Equivalent to Veto.init() + veto.wrap() # Use protect() for quick setup, Veto.init() for advanced config.
Web framework integrations
Use the SDK in your API layer to authorize tool calls before they execute. Works with any Python web framework.
FastAPI
Initialize Veto once on startup. Use veto.guard() in your route handlers for pre-flight validation without wrapping tools.
from fastapi import FastAPI, HTTPException
from veto import Veto, ToolCallDeniedError
from pydantic import BaseModel
app = FastAPI()
veto: Veto | None = None
@app.on_event("startup")
async def startup():
global veto
veto = await Veto.init(api_key="veto_live_...")
class AgentRequest(BaseModel):
tool_name: str
arguments: dict
@app.post("/agent/execute")
async def execute_tool(req: AgentRequest):
decision = await veto.guard(req.tool_name, req.arguments)
if decision.decision == "deny":
raise HTTPException(status_code=403, detail=decision.reason)
if decision.decision == "require_approval":
return {
"status": "pending_approval",
"approval_id": decision.approval_id,
"message": decision.reason,
}
result = await run_tool(req.tool_name, req.arguments)
return {"status": "executed", "result": result}Django
Django's synchronous views work with Veto through asyncio.run(). For async views, use await directly.
# views.py
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from veto import Veto
import asyncio
import json
_veto = None
async def get_veto():
global _veto
if _veto is None:
_veto = await Veto.init()
return _veto
@csrf_exempt
def agent_execute(request):
if request.method != "POST":
return JsonResponse({"error": "Method not allowed"}, status=405)
body = json.loads(request.body)
veto = asyncio.run(get_veto())
decision = asyncio.run(
veto.guard(body["tool_name"], body["arguments"])
)
if decision.decision == "deny":
return JsonResponse({"error": decision.reason}, status=403)
result = execute_tool(body["tool_name"], body["arguments"])
return JsonResponse({"result": result})Provider adapters
Built-in adapters convert between Veto's tool format and provider-specific formats. Define tools once, use them with any LLM provider.
OpenAI
from veto import Veto, to_openai_tools, from_openai_tool_call
from openai import AsyncOpenAI
client = AsyncOpenAI()
veto = await Veto.init()
# Define tools and wrap with Veto
my_tools = [search_web, send_email, read_database]
wrapped_tools = veto.wrap(my_tools)
# Convert to OpenAI format
openai_tools = to_openai_tools(wrapped_tools)
response = await client.chat.completions.create(
model="gpt-5.4",
messages=[{"role": "user", "content": "Search for Q3 revenue data"}],
tools=openai_tools,
)
# When OpenAI returns a tool call, convert and validate
for tool_call in response.choices[0].message.tool_calls:
veto_call = from_openai_tool_call(tool_call)
# Authorization already enforced by wrapped tools
result = await execute_wrapped(veto_call)Anthropic Claude
from veto import Veto, to_anthropic_tools, from_anthropic_tool_use
import anthropic
client = anthropic.AsyncAnthropic()
veto = await Veto.init()
wrapped_tools = veto.wrap([search_web, send_email])
claude_tools = to_anthropic_tools(wrapped_tools)
response = await client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": "Send the report to the team"}],
tools=claude_tools,
)
for block in response.content:
if block.type == "tool_use":
veto_call = from_anthropic_tool_use(block)
result = await execute_wrapped(veto_call)Agent framework patterns
Veto wraps tools at the boundary between your agent framework and the outside world. The framework keeps working normally. Authorization happens transparently.
LangChain / LangGraph
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
from pydantic import BaseModel, Field
from veto import Veto
class TransferInput(BaseModel):
amount: float = Field(description="Amount to transfer")
to_account: str = Field(description="Destination account ID")
@tool("transfer_money", args_schema=TransferInput)
def transfer_money(amount: float, to_account: str) -> str:
"""Transfer money to a bank account."""
return f"Transferred $" + str(amount) + f" to {to_account}"
veto = await Veto.init()
wrapped_tools = veto.wrap([transfer_money])
llm = ChatOpenAI(model="gpt-5.4")
agent = create_react_agent(llm, wrapped_tools)Dataclass tools
Veto automatically detects tools defined as dataclasses with a name, description, and handler method.
from dataclasses import dataclass
from typing import Any
@dataclass
class DatabaseQuery:
name: str = "query_database"
description: str = "Run a SQL query against the production database"
async def handler(self, args: dict[str, Any]) -> str:
query = args["sql"]
# Execute query...
return f"Query returned {len(rows)} rows"
@dataclass
class FileWriter:
name: str = "write_file"
description: str = "Write content to a file on disk"
async def handler(self, args: dict[str, Any]) -> str:
path, content = args["path"], args["content"]
# Write file...
return f"Wrote {len(content)} bytes to {path}"
veto = await Veto.init()
safe = veto.wrap([DatabaseQuery(), FileWriter()])Async patterns
The SDK is async-native. Validate multiple tool calls in parallel with asyncio.gather for minimal latency.
import asyncio
from veto import Veto
async def main():
veto = await Veto.init()
# Parallel tool validation
tools = ["send_email", "delete_user", "export_data"]
args_list = [
{"to": "user@external.com", "body": "Report"},
{"user_id": "usr_123"},
{"format": "csv", "scope": "all"},
]
decisions = await asyncio.gather(*[
veto.guard(tool, args)
for tool, args in zip(tools, args_list)
])
for tool, decision in zip(tools, decisions):
if decision.decision == "allow":
print(f"{tool}: allowed")
elif decision.decision == "deny":
print(f"{tool}: blocked - {decision.reason}")
elif decision.decision == "require_approval":
print(f"{tool}: needs approval (id: {decision.approval_id})")
asyncio.run(main())Policy rules
Define authorization policies in YAML. Rules match tool calls by name, evaluate conditions against arguments and context, and enforce actions.
version: "1.0"
name: Python agent policies
rules:
- id: block-sql-mutations
action: block
tools: [query_database]
conditions:
- field: arguments.sql
operator: matches
value: "(?i)(DROP|DELETE|TRUNCATE|ALTER|UPDATE|INSERT)"
message: "Mutation queries blocked. Use read-only queries."
- id: limit-transfer-amount
action: require_approval
tools: [transfer_money]
conditions:
- field: arguments.amount
operator: greater_than
value: 1000
message: "Transfers over $1000 require human approval"
- id: restrict-file-paths
action: block
tools: [write_file]
conditions:
- field: arguments.path
operator: not_matches
value: "^/app/data/"
message: "File writes restricted to /app/data/"Features
Runtime authorization
Every tool call is validated against your policies before execution. The agent cannot bypass guardrails regardless of its reasoning.
Framework agnostic
Works with LangChain, CrewAI, PydanticAI, AutoGen, or custom agents. Adapters for OpenAI, Anthropic, and Google function calling formats.
Deterministic policies
Policies are code, not prompts. Argument limits, regex matching, rate limits. Evaluate locally without network latency.
Cloud sync
Policies managed in Veto Cloud. Real-time updates, team collaboration, approval workflows, and audit logging.
Build vs buy
| Capability | DIY | Veto Python SDK |
|---|---|---|
| Initial setup | 2-4 weeks | 5 minutes |
| Policy engine | ||
| Approval workflows | ||
| Audit logging | ||
| Provider adapters | ||
| Async-native | Manual | |
| Maintenance burden | Ongoing | None |
Frequently asked questions
How do I install the Python SDK?
Does it work with async frameworks like FastAPI?
Can I use Veto without a network connection?
How do I handle blocked tool calls?
Which Python agent frameworks are supported?
Related integrations
LangChain agent guardrails and tool authorization
CrewAICrewAI multi-agent authorization and role-based access
AutoGenMicrosoft AutoGen multi-agent authorization
Ship Python agents with confidence.