toolguard — Python SDK for Tool Guard Core
Universal AI agent governance: evaluate any tool call against configurable
policies before it executes. Drop-in adapters for LangChain, AutoGen,
native OpenAI/Anthropic tool use, and MCP. Two backends — CLI (tg evaluate)
and HTTP proxy (tg-proxy /evaluate) — so you can run locally or in production.
Install
The PyPI distribution is named tool-guard-core because the unqualified
toolguard project belongs to an unrelated package. The import remains
toolguard. Tagged releases beginning with v0.8.0 install with:
pip install "tool-guard-core" # core only (httpx)
pip install "tool-guard-core[langchain]" # + langchain-core
pip install "tool-guard-core[autogen]" # + pyautogen
pip install "tool-guard-core[openai]" # + openai SDK
pip install "tool-guard-core[anthropic]" # + anthropic SDK
Before v0.8.0, or when testing an unreleased checkout, use
pip install "./sdk/python[langchain]" from the repository root.
Quick start
from toolguard import ToolGuard, ToolDenied
guard = ToolGuard(
mode="proxy", # or "cli" to run tg locally
proxy_url="http://localhost:9090",
agent_id="my-agent",
org_id="acme",
)
try:
result = guard.evaluate("issue_refund", {"amount": 1000})
# allowed — call the real tool
except ToolDenied as exc:
# exc.result.decision_reason, exc.result.suggested_response
print("Blocked:", exc)
CLI mode (no running proxy)
guard = ToolGuard(
mode="cli",
tg_bin="/usr/local/bin/tg", # or just "tg" if on PATH
policy_dir="/etc/tg/policies", # directory of *.yaml policies
agent_id="my-agent",
org_id="acme",
)
result = guard.evaluate_raw("issue_refund", {"amount": 100})
print(result.decision) # "allowed"
CLI mode evaluates the whole policy directory as one set. Each policy's YAML
mode owns its contribution: a mode: shadow policy remains observe-only under
the CLI's default enforcement call-site mode, while a mode: enforcement
policy cannot be downgraded by a shadow default. Mixed-mode behavior is pinned
against the real Go engine in tests/test_contract.py.
evaluate() raises ToolDenied or ToolEscalated on a block.
evaluate_raw() always returns the EvaluationResult without raising.
LangChain / LangGraph
from toolguard import ToolGuard
from toolguard.adapters.langchain import ToolGuardCallbackHandler
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor
guard = ToolGuard(mode="proxy", proxy_url="http://localhost:9090",
agent_id="lc-agent", org_id="acme")
handler = ToolGuardCallbackHandler(
guard,
tool_groups={"issue_refund": "monetary_outflow", "search": "read_ops"},
)
executor = AgentExecutor(agent=agent, tools=tools, callbacks=[handler])
# Every tool call goes through policy evaluation before execution.
The handler raises ToolDenied / ToolEscalated on a block, stopping
the chain. Flagged calls (recorded near-misses) proceed.
Microsoft AutoGen
from toolguard import ToolGuard
from toolguard.adapters.autogen import guarded
guard = ToolGuard(mode="proxy", proxy_url="http://localhost:9090",
agent_id="autogen-agent", org_id="acme")
@guarded(guard, tool_name="issue_refund", tool_group="monetary_outflow")
def issue_refund(order_id: str, amount: float) -> dict:
return {"status": "refunded", "amount": amount}
# Register with AutoGen as normal:
assistant.register_for_execution(name="issue_refund")(issue_refund)
On deny/escalate, guarded returns {"error": "denied", "reason": "...", "suggested_response": "..."} — the conversation loop surfaces it to the LLM.
Native OpenAI tool use
import openai
from toolguard import ToolGuard
from toolguard.adapters.native import guard_tool_calls
client_oai = openai.OpenAI()
guard = ToolGuard(mode="proxy", proxy_url="http://localhost:9090",
agent_id="openai-agent", org_id="acme")
response = client_oai.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
)
allowed, denied = guard_tool_calls(
response.choices[0].message.tool_calls,
guard,
provider="openai",
tool_groups={"issue_refund": "monetary_outflow"},
)
tool_results = []
for item in allowed:
call = item["call"]
output = MY_TOOLS[call.function.name](**json.loads(call.function.arguments))
tool_results.append({"role": "tool", "tool_call_id": call.id, "content": str(output)})
for item in denied:
tool_results.append(item["tool_result"]) # role=tool, error message
Native Anthropic tool use
import anthropic
from toolguard import ToolGuard
from toolguard.adapters.native import guard_tool_calls
client_ant = anthropic.Anthropic()
guard = ToolGuard(mode="proxy", proxy_url="http://localhost:9090",
agent_id="anthropic-agent", org_id="acme")
response = client_ant.messages.create(
model="claude-opus-4-5",
max_tokens=4096,
tools=tools,
messages=messages,
)
tool_use_blocks = [b for b in response.content if b.type == "tool_use"]
allowed, denied = guard_tool_calls(tool_use_blocks, guard, provider="anthropic")
tool_results = []
for item in allowed:
call = item["call"]
output = MY_TOOLS[call.name](**call.input)
tool_results.append({"type": "tool_result", "tool_use_id": call.id, "content": str(output)})
for item in denied:
tool_results.append(item["tool_result"]) # type=tool_result, is_error=True
MCP server
from toolguard import ToolGuard
from toolguard.adapters.mcp import guard_mcp_tool
guard = ToolGuard(mode="proxy", proxy_url="http://localhost:9090",
agent_id="mcp-server", org_id="acme")
@guard_mcp_tool(guard, tool_group="data_access")
def call_tool(tool_name: str, parameters: dict, **kwargs) -> dict:
return TOOLS[tool_name](**parameters)
Errors
| Exception | When raised |
|---|---|
ToolDenied |
action_taken is anything other than allowed, allowed_shadow, flagged, or escalated (i.e. the call was actually blocked) |
ToolEscalated |
action_taken == "escalated" (wait for human approval) |
Raised on action_taken, not decision — deliberately. In shadow mode
the engine reports decision="denied" (what would happen) alongside
action_taken="allowed_shadow" (what actually happened, since shadow
mode never enforces); evaluate() never raises for that case, since
shadow mode exists specifically to observe without blocking.
Both carry a .result attribute with the full EvaluationResult:
result.decision_reason, result.suggested_response, result.rule_results, etc.
Wire contract
The SDK mirrors the Go pkg/domain JSON contract exactly. Key field names:
ActionEnvelope (what the SDK sends to the engine):
envelope_id, timestamp, agent_id, session_id, org_id,
framework ("mcp" | "langgraph" | "sdk" | "unknown"),
tool_name, tool_group, parameters, context.verified,
context.session_state, integration_type ("mcp_proxy" |
"langgraph_middleware" | "sdk").
EvaluationResult (what the engine returns):
decision ("allowed" | "denied" | "escalated" | "flagged"),
action_taken, decision_reason, effective_mode, policies_matched,
rules_evaluated, rules_triggered, rule_results, applied_rule_results,
primary_citation, applied_primary_citation, is_near_miss,
suggested_response, and the proxy-only optional decision_receipt. The
applied provenance fields explain action_taken;
the aggregate fields can point to stricter shadow-only telemetry.
DecisionReceipt (after a successful proxy audit append):
receipt_version, trace_id, trace_hash, hash_algorithm,
canonical_trace_version, integrity_model, decision, action_taken,
timestamp, optional issuer, and the opaque receipt_uri.
Escalation (poll/approve/deny responses): id, state, timestamps,
approver fields, envelope, decision, and an optional
resolution_receipt after the terminal resolution was audited.
Decision-receipt helper
from toolguard.adapters.memory import with_receipt_reference
result = guard.evaluate_raw("issue_refund", {"amount": 100})
event = with_receipt_reference(
{"tool": "issue_refund", "outcome": result.action_taken}, result
)
The helper adds tool_guard_receipt_uri only when the proxy supplied a real
receipt. It performs no storage or network operation and never fabricates a
fallback identifier. Absence is never authorization. The URN is a
tamper-evident hash-chain reference, not a signature or fetchable URL; see the
full contract.
Development
cd sdk/python
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytest tests/ --cov-fail-under=90
Or via the top-level Makefile:
make sdk-test
Metadata
Release files for tool-guard-core 0.8.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 | |
|---|---|---|---|---|
| tool_guard_core-0.8.0.tar.gz | 50.8 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tool_guard_core-0.8.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 81.4 kB
Release files / tool_guard_core-0.8.0.tar.gz
| Download URL | tool_guard_core-0.8.0.tar.gz |
|---|---|
| Size | 50.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
10f8ab399b39535f4f72016fed8250bf6eb3c7a4f96d90eae3bbf7cfe0a8ba82
|
|
BLAKE2b-256 checksum How to use checksums |
6fc5a615fc1b20b29e9ac865c5c6fd78276c5191817dc190c661303334083464
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency logRelease files / tool_guard_core-0.8.0-py3-none-any.whl
| Download URL | tool_guard_core-0.8.0-py3-none-any.whl |
|---|---|
| Size | 30.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fc3b741fb6a3d8a24dad933f3a89442e833d8186e7753ebd76a30488be6f7ba8
|
|
BLAKE2b-256 checksum How to use checksums |
c00ae96ccb75561354d8a06ec4eedb3b67fae01f7c052c33b10a4c5026624628
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency logSecurity for tool-guard-core 0.8.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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for tool_guard_core-0.8.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency logA 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.