ToolGate
AI Agent Security & Authorization SDK
ToolGate is an authorization layer that sits between an AI agent and the tools it is allowed to call. Every tool call the agent proposes is evaluated before it runs and receives exactly one decision:
ALLOW · DENY · ESCALATE
Decisions are made by a deterministic, rule-based policy engine that reasons about who is really asking (instruction provenance), what the user actually authorized (declared scope), and what the session has already done (cumulative risk and chain effects). An LLM may be consulted as an advisory signal, but it never decides.
AI Agent
↓
ToolGate SDK
↓
Authorization / Policy / Risk checks
↓
ALLOW / DENY / ESCALATE
↓
Your real tool
Install
pip install toolgate-sdk
The core package has no third-party dependencies. Optional extras:
| Extra | Adds | Install |
|---|---|---|
dashboard |
Live decision dashboard + HTTP API (FastAPI, uvicorn) | pip install "toolgate-sdk[dashboard]" |
langchain |
LangChain / LangGraph tool adapter | pip install "toolgate-sdk[langchain]" |
llm |
LLM-backed advisor (Groq or Anthropic) | pip install "toolgate-sdk[llm]" |
all |
Everything above | pip install "toolgate-sdk[all]" |
Quick start
from toolgate import guard_tools, TaskSession, ToolDenied, ToolEscalated
# Your ordinary tools. Nothing about ToolGate in here.
def read_file(path: str) -> str:
return open(path).read()
def send_email(to: str, body: str) -> str:
... # actually send it
return "sent"
# 1. Guard them. Category/operation are inferred from the name
# (read_file -> file_ops.read, send_email -> messaging.send).
protected_tools = guard_tools([read_file, send_email])
# 2. One user task == one session. Declare intent and scope.
with TaskSession(intent="Summarize the Q3 report", scope={"read:file_ops"}):
agent = Agent(tools=protected_tools) # any framework that calls Python callables
agent.run("Summarize reports/q3_summary.txt")
Inside the session, every call to a protected tool goes through the policy engine:
- ALLOW – the function runs and returns normally.
- DENY –
ToolDeniedis raised; the function never runs. - ESCALATE –
ToolEscalatedis raised. After a human approves, callsession.confirm(exc)to run the original call exactly once.
with TaskSession(intent="Send the invoice total to finance",
scope={"read:file_ops", "send:messaging"}) as session:
try:
send_email(to="finance@corp.example", body="Total: $4,120")
except ToolEscalated as e:
if human_approves(e.result.reason):
session.confirm(e) # runs the call, records the approval
except ToolDenied as e:
print("blocked:", e.result.reason)
Why this works
ToolGate treats authority and data as different things.
| Source | Trust | Can direct the agent? |
|---|---|---|
| The user's request | USER |
yes |
| Agent configuration | SYSTEM |
narrowly |
| Output of an earlier tool | TOOL_OUTPUT |
no |
| Web pages, documents, emails | RETRIEVED_CONTENT |
no |
| Tool / plugin descriptions | THIRD_PARTY_METADATA |
no |
A document can supply a phone number to put in a message the user asked to send. It can never supply the instruction to send a message. This distinction is what stops prompt injection from turning into action.
Labeling provenance explicitly
When you know where a value came from, say so. Unlabeled values default
to USER provenance, so labeling is what gives the engine its leverage.
from toolgate import guarded_tool, untrusted
@guarded_tool(category="messaging", operation="send")
def send_message(to: str, body: str): ...
with TaskSession(intent="Reply to the vendor", scope={"send:messaging"}) as s:
# A value that came out of a document:
send_message(to=untrusted("evil@example.com", origin="invoice.pdf"), body="...")
# The agent is acting BECAUSE of something it read, not because the user asked:
with s.acting_on_retrieved_content("invoice.pdf"):
send_message(...) # directive provenance = the document -> DENY if out of scope
For tools that read untrusted content and return something derived from it (a summary, an extraction), scan the raw content first:
from toolgate import scan_content
def summarize_document(name: str) -> str:
text = read(name)
scan_content(text, origin=name) # raises ToolDenied and quarantines on injection
return llm.summarize(text)
The rules
Evaluated in order. The first conclusive rule wins.
| Rule | What it catches |
|---|---|
| R0 Stale authorization | A human approval covers one action in one context. Change the destination, resource, phase, scope or who is directing, and it is re-evaluated. |
| R1 / R1b Directive authority | The instruction to act came from content that cannot grant authority (a document, a tool result, plugin metadata). |
| R2 Scope | The action is outside the user's declared scope, or ambiguous relative to their intent. |
| R3 Base risk | Each category/operation carries a severity weight. High-severity single actions (delete, exec, drop) escalate by default. |
| R4 Novelty | A resource or target never touched before in this session raises the risk. |
| R5 Cumulative risk | The session as a whole has accumulated too much risk. |
| R6 Dangerous combinations | Known-dangerous sequences, such as reading sensitive data then sending externally, flagged even though each step passes alone. |
| R7 Chain effects | The union of everything the session has already done, plus this action, completes a data-exfiltration or credential-pivot chain the user never authorized. |
| R8 Output quarantine | A tool's output contains an embedded instruction. It is quarantined and never reaches the model. |
Every evaluation is recorded to the session audit log with the rule that fired and a human-readable reason.
Human approvals are not standing permissions
session.confirm(exc) records a grant bound to that exact action and
context. A later call is only covered if it is identical and the grant
was issued with reusable=True. Anything else goes back through the
engine, and the audit entry says exactly what changed.
session.confirm(e, reusable=True) # identical later calls skip re-confirmation
session.enter_phase("review", scope={"read:file_ops"}) # every earlier approval is now stale
LangChain / LangGraph
pip install "toolgate-sdk[langchain]"
from toolgate import TaskSession
from toolgate.integrations.langchain import guard_langchain_tools
safe_tools = guard_langchain_tools(my_tools, soft_deny=True)
agent = create_react_agent(llm, safe_tools)
with TaskSession(intent="Summarize the invoice", scope={"read:file_ops"}):
agent.invoke({"messages": [...]})
soft_deny=True returns the denial reason as the tool's output instead of
raising, so the model can explain to the user why it stopped.
Dashboard
pip install "toolgate-sdk[dashboard]"
import toolgate
toolgate.serve_dashboard() # background thread on http://127.0.0.1:8000
Every session in the process appears live, with each decision, the rule
that fired, and the running risk score. toolgate-dashboard starts the
same server from the command line, and the HTTP API lets non-Python
agents propose actions over REST.
LLM advisor (optional)
The policy engine can consult an LLM for two narrow questions: does this
action semantically match the user's intent, and does this text look
like an embedded instruction. Set GROQ_API_KEY or ANTHROPIC_API_KEY
and install the llm extra to enable it. Without a key, a transparent
keyword heuristic is used and the whole system runs offline. The LLM is
advisory only; the rules always have the final word.
Limitations
- Unlabeled parameters default to
USERprovenance. ToolGate can never conjure labels the caller did not provide. Theguard_toolsadapter narrows this window with output quarantine, but explicit labeling is stronger. - Category and operation inference from tool names is a heuristic. Use
overrides={"tool_name": ("file_ops", "delete")}when it is wrong. - The session store is in-memory. Swap it for a persistent store for multi-process deployments.
Public API
from toolgate import (
guard_tools, guard_callable, guarded_tool, scan_content,
TaskSession, current_session,
untrusted, from_tool_output, from_tool_metadata, Sourced,
ToolDenied, ToolEscalated, AuthorizationError,
Decision, ToolCategory, TrustLevel, ProposedAction, PolicyResult,
AuthorizationMediator, PolicyEngine, SessionContext, SessionStore,
serve_dashboard, stop_dashboard,
)
License
MIT. See LICENSE.
Metadata
Release files for toolgate-sdk 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| toolgate_sdk-0.1.0.tar.gz | 43.8 kB | Without provenance | Details for toolgate_sdk-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| toolgate_sdk-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for toolgate_sdk-0.1.0-py3-none-any.whl |
Total release size: 90.6 kB
Release files / toolgate_sdk-0.1.0.tar.gz
| Download URL | toolgate_sdk-0.1.0.tar.gz |
|---|---|
| Size | 43.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e43a6c5f939bf2de50ec7192273d43aad34aae148deabdde4be24f4f9e2730dc
|
|
BLAKE2b-256 checksum How to use checksums |
5b22684d40bfa39bd49fd7b50f94d56b9541d62e6f298e0ca56978e8aed1f0a4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.4
|
Release files / toolgate_sdk-0.1.0-py3-none-any.whl
| Download URL | toolgate_sdk-0.1.0-py3-none-any.whl |
|---|---|
| Size | 46.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
29238b160cf0512ae40ffe26381055c64c0a85e13d091c03b847da9fa5512975
|
|
BLAKE2b-256 checksum How to use checksums |
9c3828a50b6154385e74fae0f274b3c8f03d80d74d837045a618749127424d0e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.4
|
Security for toolgate-sdk 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.