Python preview package for local tool-call checks
Use Python for local tool-call checks while production integrations use the TypeScript SDK, HTTP API, or MCP gateway. Each governed callable is evaluated before dispatch: allow, review, or deny, with an exportable decision record when synced.
Installation
pip install veto-sdk
Requires Python 3.9+. This preview is intended for local tool-call checks and custom runtimes; production framework guides use TypeScript, HTTP, or MCP when those paths are the stable surface.
Install the SDK
Initialize Veto, wrap your tools, and call them normally. Blocked calls raise ToolCallDeniedError with the policy reason attached.
import asyncio
import os
from veto_sdk 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 one helper. Good for a minimal wrapper and scripts.
from veto_sdk import protect # Callable wrapping. Reads VETO_API_KEY from env. safe_tools = await protect([transfer_money, delete_file]) # Equivalent to Veto.init() + veto.wrap() # Use protect() for a minimal wrapper, Veto.init() for advanced config.
Web framework integrations
Use the SDK in your API layer to check 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
import os
from veto_sdk 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=os.environ["VETO_API_KEY"])
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
import os
from veto_sdk 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
import os
from veto_sdk 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=os.environ["OPENAI_MODEL"],
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)
# Guardrails already enforced by governed tools
result = await execute_wrapped(veto_call)Anthropic Claude
import os
from veto_sdk 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=os.environ["ANTHROPIC_MODEL"],
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. Enforcement happens transparently.
LangChain and LangGraph
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
from pydantic import BaseModel, Field
import os
from veto_sdk 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=os.environ["OPENAI_MODEL"])
agent = create_react_agent(llm, wrapped_tools)Dataclass tools
Veto can detect 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 here.
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 here.
return f"Wrote {len(content)} bytes to {path}"
veto = await Veto.init()
guarded_tools = veto.wrap([DatabaseQuery(), FileWriter()])Async patterns
The SDK is async-native. Validate multiple tool calls in parallel with asyncio.gather while policy decisions stay in-process before dispatch.
import asyncio
import os
from veto_sdk import Veto
async def main():
veto = await Veto.init()
# Parallel tool validation
tools = ["send_email", "delete_user", "export_data"]
args_list = [
{"to": "user@external.invalid", "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 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
Each governed tool call is validated against your policies before execution. Policy enforcement runs outside the agent's reasoning path.
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 on the local in-process path.
Cloud sync
Policies managed in Veto Cloud. Shared policy updates, team collaboration, approval workflows, and decision records.
Build vs buy
| Capability | DIY | Veto Python preview |
|---|---|---|
| Initial setup | Custom build | First governed call |
| Policy engine | ||
| Approval workflows | ||
| Decision records | ||
| Provider adapters | ||
| Async-native | Manual | |
| Maintenance burden | Ongoing | None |
Frequently asked questions
How do I install the Python preview package?
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 tool-level policy enforcement
CrewAICrewAI multi-agent guardrails and role-based access
AutoGenMicrosoft AutoGen multi-agent guardrails
Run Python agents with policy.