Quick start
This page wraps a compiled LangGraph and shows what happens on each decision. The same pattern applies to the other frameworks; only the object passed to wrap_agent and the framework key change.
flowchart LR
A["wrap_agent()"] --> B["Agent invokes tool"]
B --> C["GATE guard intercepts"]
C --> D{"All 5 stages pass?"}
D -->|Yes| E["ALLOW
Tool runs normally"]
D -->|No| F["BLOCK
ToolExecutionBlocked raised"]
D -->|Uncertain| G["ESCALATE
Held for human review"]
style E fill:#e6f9ed,stroke:#2eb85c,color:#006622
style F fill:#fde2e2,stroke:#d62728,color:#990000
style G fill:#fff3e0,stroke:#f59f00,color:#995c00
A guarded LangGraph
from langchain_core.tools import tool
from langgraph.graph import END, START, StateGraph
from langgraph.prebuilt import ToolNode
from gate import AgentProfile, ResourceBudget, gateConfig, wrap_agent
@tool
def search(query: str) -> str:
"""Search the internal knowledge base."""
return f"results for {query}"
@tool
def delete_records(table: str) -> str:
"""Delete every row in a table."""
return f"deleted {table}"
builder = StateGraph(dict)
builder.add_node("tools", ToolNode([search, delete_records]))
builder.add_edge(START, "tools")
builder.add_edge("tools", END)
graph = builder.compile()
config = gateConfig(
agent_profile=AgentProfile(
agent_id="research-agent",
role="analyst",
allowed_tools={"search"},
resource_budget=ResourceBudget(max_steps=20, max_loops_per_phase=5),
),
enabled_checks={"privilege", "dfa"},
)
secure = wrap_agent(graph, "langgraph", config)
The example enables only the deterministic stages so it runs without downloading an embedding model. search is allowed, delete_records is not on the allow-list.
What a blocked call looks like
When the model asks for a forbidden tool, the framework’s dispatch reaches the guard, the monitor denies the call, and the tool body never runs:
from gate.adapters import ToolExecutionBlocked
try:
secure.invoke({"messages": [ai_message_with_tool_call("delete_records", {"table": "users"})]})
except ToolExecutionBlocked as exc:
print(exc.tool_name) # delete_records
print(exc.result.checks_failed) # ['privilege']
print(exc.result.reason) # Privilege violation: ...
print(exc.post_execution) # False
ToolExecutionBlocked extends SecurityViolation and carries the monitor’s ValidationResult in .result. How the surrounding framework reacts depends on the framework: LangGraph and LangChain let the exception propagate through invoke, while a host application may catch it and report the refusal to the model.
The tool history stays empty because the call never executed:
assert secure.get_state().tool_history == []
Inspecting an allowed call
result = secure.invoke({"messages": [ai_message_with_tool_call("search", {"query": "retention policy"})]})
state = secure.get_state()
print(state.current_phase) # ANSWERING after invoke returns
print([call.tool_name for call in state.tool_history]) # ['search']
print(state.step_count) # 1
After invoke finishes, finish_run() moves the run to ANSWERING. A tool call attempted after that point is refused by the DFA guard even if its privilege check would pass.
Enabling the semantic stage
The default gateConfig enables all five stages. The semantic firewall needs harm prototypes and safe centroids before its score is meaningful; without them, one of its three drift terms is a constant and the score mostly reflects drift from the run’s intent anchor.
config = gateConfig(
agent_profile=profile,
enabled_checks={"privilege", "dfa", "contracts", "semantic", "dcbf"},
vector_store_path="gate-vectors", # directory containing the .npy prototypes
)
Populate the vector store before constructing the monitor, or attach prototypes to the firewall after construction. The full procedure and the exact scoring formula are in Semantic firewall.
The D-CBF stage has its own defaults. In particular, the default privacy barrier turns negative after the first send_email or http_request call and then blocks subsequent actions. Read D-CBF monitor before enabling it on a tool set that sends mail or makes outbound requests.
Adding a contract
Contracts are looked up by exact tool name. A contract applies to any tool with that name, regardless of framework.
from gate.security.smart_contracts import BaseSmartContract, postcondition
class SearchOutputContract(BaseSmartContract):
@postcondition
def output_is_text(self, action, result, state):
return isinstance(result, str)
secure.monitor.contract_registry.register_contract("search", SearchOutputContract())
With the contracts check enabled, a postcondition runs after the tool body and can withhold the output from the agent. It cannot undo the side effect. See Smart contracts.
Routing an escalation to a human
An ESCALATE decision is not a refusal yet. The call is withheld until a human resolves it. Pass an approval queue to wrap_agent:
from gate.security.hitl_queue import HumanApprovalQueue, SlackNotificationChannel
queue = HumanApprovalQueue(
notification_channels=[
SlackNotificationChannel("https://hooks.slack.com/services/...", "#agent-approvals")
],
default_sla_seconds=300,
)
secure = wrap_agent(graph, "langgraph", config, approval_queue=queue)
The approval blocks the tool call until someone resolves the request. A separate thread can resolve it:
import threading
import time
def approver():
while True:
pending = queue.get_pending_requests()
if pending:
secure.enforcement.approve(pending[0].request_id, "alice", "looks fine")
return
time.sleep(0.1)
threading.Thread(target=approver, daemon=True).start()
result = secure.invoke({"messages": [...]})
If no queue is configured, an escalated call is refused with ToolExecutionBlocked and the reason “no human approval queue is configured”. If the queue is full, the SLA expires, or the approver rejects the request, the call is denied. See Human-in-the-loop.
Other frameworks
The configuration and decision model are the same in every adapter. Framework scenarios runs this scenario end to end in all four frameworks, with a complete keyless driver for each.
# LangChain AgentExecutor or compiled agent graph
secure = wrap_agent(agent, "langchain", config)
result = secure.invoke("Summarise the report")
# OpenAI Agents SDK
secure = wrap_agent(agent, "openai-agents", config)
result = await secure.invoke("Summarise the report") # async only
# CrewAI
secure = wrap_agent(crew, "crewai", config)
result = secure.invoke({"topic": "retention"})
If a LangChain or LangGraph graph does not expose its tools through a ToolNode or a .tools attribute, pass them explicitly:
secure = wrap_agent(graph, "langchain", config, tools=[search, delete_records])
An explicit tools= list is also the way to guard tools that live in a nested graph the adapter cannot see. Only guarded tools are mediated; a tool the adapter never receives runs unguarded.
Clean up
SecureAgent.terminate() closes the dedicated monitor event loop and releases its thread:
secure.terminate()
Call it when the wrapped agent is no longer needed. The event loop thread is a daemon, so process exit does not hang without it, but long-lived processes that create many wrapped agents should terminate them explicitly.
Next steps
- Architecture explains the exact call sequence and failure semantics.
- Configuration lists which configuration fields the runtime actually reads.
- Security model states what GATE does and does not guarantee.