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.

Python runtime authorization, veto-sdk Python preview, Python callable authorization, LangChain guardrails, CrewAI guardrails, PydanticAI guardrails, FastAPI agent tools, Django agent guardrails

Installation

Terminal
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.

agent.py
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.

Callable wrapping
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.

FastAPI integration
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.

Django integration
# 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

OpenAI function calling
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

Claude tool use
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

LangChain tool wrapping
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.

Dataclass tool wrapping
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.

Parallel validation
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.

veto/policies.yaml
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

CapabilityDIYVeto Python preview
Initial setupCustom buildFirst governed call
Policy engine
Approval workflows
Decision records
Provider adapters
Async-nativeManual
Maintenance burdenOngoingNone

Frequently asked questions

How do I install the Python preview package?
Install via pip: pip install veto-sdk. Import it as veto_sdk. Set the VETO_API_KEY environment variable with your API key from the workspace. Initialize with await Veto.init() in async code, or use the protect() shorthand for scripts.
Does it work with async frameworks like FastAPI?
Yes. The SDK is async-native. All methods (init, wrap, guard) are async and work directly in FastAPI, Starlette, and any asyncio-based framework. For synchronous frameworks like Django, use asyncio.run() or async views.
Can I use Veto without a network connection?
For deterministic policies, yes. Define constraints locally and they evaluate without network calls. Cloud policies require connectivity for shared policy updates and approval workflows.
How do I handle blocked tool calls?
Blocked calls raise ToolCallDeniedError with a reason attribute. Catch it and return a fallback response, log the attempt, or escalate to human review. Use veto.guard() for pre-flight checks that return a decision object instead of raising.
Which Python agent frameworks are supported?
Use the preview package for local callable checks and custom wrappers. For production framework support, use the TypeScript guides, the HTTP API, or the MCP gateway until dedicated Python framework adapters are published.

Related integrations

Browse integrations

Run Python agents with policy.