LangGraph runtime authorization
Wrap LangGraph tool nodes with Veto. Each governed tool node is evaluated before dispatch: allow, review, or deny, with an exportable decision record per governed decision.
The problem with LangGraph tool execution
LangGraph gives agents state machines, checkpointing, and multi-agent coordination. But its ToolNode executes tool calls with no authorization check. The LLM picks a tool, the ToolNode runs it. In a multi-agent graph, once one agent hands work to another, there is no built-in mechanism for scoped delegation or tool-level enforcement.
This matters because LangGraph is where high-stakes agents live: financial workflows, customer operations, and infrastructure automation. LangGraph's interrupt() provides human review at the graph level, but it does not evaluate each tool argument against policy before execution.
LangGraph's prebuilt ToolNode executes the tool calls the LLM produces. It parses JSON arguments and runs the function. No policy. No validation. No approval.
In multi-agent graphs, agents share tools without boundaries. A research agent could call a payment tool if the LLM decides to. There is no role-based isolation.
Stateful graphs carry context across steps. Tools exposed to LLM-generated arguments need external policy so one node cannot turn stale context into a side effect.
Before and after Veto
The left tab shows a standard LangGraph agent built with create_react_agent. The ToolNode executes requested tool calls without a policy decision. The right tab adds Veto inside each tool function.
import os
from langgraph.prebuilt import create_react_agent
from langchain_core.tools import tool
@tool
def process_payment(amount: float, customer_id: str) -> str:
"""Process a payment for a customer."""
return payment_service.charge(amount, customer_id)
@tool
def query_database(query: str, tables: list[str]) -> str:
"""Query the customer database."""
return db.execute(query, tables)
@tool
def delete_records(table: str, condition: str) -> str:
"""Delete records matching a condition."""
return db.execute(f"DELETE FROM {table} WHERE {condition}")
# create_react_agent builds a ToolNode internally
# Each governed tool call from the LLM is executed without guardrails
agent = create_react_agent(
model=f"openai:{os.environ['OPENAI_MODEL']}",
tools=[process_payment, query_database, delete_records],
)
# The LLM decides what to call. LangGraph's ToolNode executes it.
# Prompt injection could trigger delete_records on the users table.
result = agent.invoke(
{"messages": [{"role": "user", "content": user_message}]},
)Multi-agent graph with per-agent policies
LangGraph's power is multi-agent coordination. Veto adds role-based guardrails: a researcher agent gets read-only policies while an executor agent gets write access with approval requirements. Same tools, different policies per agent context.
import os
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import create_react_agent
from langchain_core.tools import tool
from veto_sdk import Veto
veto = Veto(api_key=os.environ["VETO_API_KEY"])
@tool
def search_web(query: str) -> str:
"""Search the web for information."""
decision = veto.guard(
tool="search_web",
arguments={"query": query},
context={"agent": "researcher"},
)
if decision.action != "allow":
return f"Blocked: {decision.reason}"
return web_search.run(query)
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email to a recipient."""
decision = veto.guard(
tool="send_email",
arguments={"to": to, "subject": subject, "body": body},
context={"agent": "executor"},
)
if decision.action == "require_approval":
return f"Email requires approval (ID: {decision.approval_id})"
if decision.action != "allow":
return f"Blocked: {decision.reason}"
return email_service.send(to, subject, body)
researcher = create_react_agent(model=f"openai:{os.environ['OPENAI_MODEL']}", tools=[search_web])
executor = create_react_agent(model=f"openai:{os.environ['OPENAI_MODEL']}", tools=[send_email])
def router(state: MessagesState):
last = state["messages"][-1].content
if "send" in last.lower() or "email" in last.lower():
return "executor"
return "researcher"
workflow = StateGraph(MessagesState)
workflow.add_node("researcher", researcher)
workflow.add_node("executor", executor)
workflow.add_conditional_edges(START, router)
workflow.add_edge("researcher", END)
workflow.add_edge("executor", END)
graph = workflow.compile()Policy configuration
Policies can condition on agent identity, user role, environment, and tool arguments. Define different rules for different agents in the same graph.
rules:
- name: block_destructive_writes
description: Prevent DELETE in production
tool: delete_records
when: context.environment == "production"
action: deny
message: "Destructive writes blocked in production"
- name: approve_large_payments
description: Human approval for payments over $1,000
tool: process_payment
when: args.amount > 1000
action: require_approval
approvers: [finance-team]
timeout: 30m
- name: viewer_payment_block
description: Viewers cannot process any payments
tool: process_payment
when: context.user_role == "viewer"
action: deny
message: "Viewers cannot process payments"
- name: restrict_sensitive_tables
description: Block access to credentials tables
tool: query_database
when: '"credentials" in args.tables || "passwords" in args.tables'
action: deny
message: "Access to sensitive tables is prohibited"
- name: executor_external_email_approval
description: Require approval for external emails
tool: send_email
when: context.agent == "executor" && !args.to.endswith("@approved.example")
action: require_approval
approvers: [compliance-team]
- name: researcher_no_actions
description: Researcher agent cannot take actions
tool: send_email
when: context.agent == "researcher"
action: deny
message: "Researcher agent cannot send emails"First governed call
Install the SDK
pip install veto-sdk langgraph langchain-openaiDefine policies
Create veto/policies.yaml with rules per tool and agent context.
Add veto.guard() to each tool
Call veto.guard() at the top of each tool function. The policy check sits at the tool boundary, so graph topology and checkpointing stay outside the policy surface.
What Veto covers for LangGraph agents
Per-agent policies
Different agents in the same graph get different guardrail rules. Pass agent identity as context. A researcher gets read-only; an executor gets write with approval.
Works with checkpointing
Veto's policy decisions are stateless. They work with LangGraph's checkpoint and persistence system. Resume from a checkpoint and the policy re-evaluates.
Complements interrupt()
LangGraph's interrupt() pauses the graph for human review. Veto's require_approval pauses a single tool call. Use interrupt() for graph-level decisions, Veto for tool-level enforcement.
Decision record
Governed tool calls can log agent context, arguments, decision, and timestamp. See which agent attempted what and what was allowed.
Frequently asked questions
How does Veto integrate with LangGraph's state machine?
Can I use different policies for different graph nodes?
How do approval workflows work with LangGraph's execution model?
Does Veto work with LangGraph's streaming mode?
Related integrations
Wrap one LangGraph tool path and inspect the decision record.