Skip to main content

ToolBoundary

Runtime boundary enforcement for AI agents — as a library, not a service.

PyPI License: MIT Python 3.9+ Tests Coverage

ToolBoundary answers one question, fast and locally, every time your agent tries to call a tool: "is this exact call allowed, right now?"

No separate web app. No database to stand up. No dashboard to log into. No subscription. Your policy is plain Python, version-controlled with the rest of your code.

pip install toolboundary

What's new in v1.0.1

ToolBoundary v1.0.1 introduces a provider-neutral external authorization and evidence layer — the foundation for integrating external authorization providers (like AgentKey) while keeping ToolBoundary as the local enforcement authority.

Key additions

  • EvidenceProvider protocol — a clean interface any external provider can implement to add authorization and execution evidence without coupling to a specific vendor
  • Provider-aware execution lifecycle — authorize_call() / record_execution() freeze the exact tool call, compute the current ToolBoundary call digest, consult an optional provider, and record execution evidence
  • Observe / Enforce modes — ProviderMode.OBSERVE logs provider decisions without blocking; ProviderMode.ENFORCE fails closed on provider denial or unavailability
  • evaluate() method — returns a structured LocalDecision instead of raise-on-deny, enabling richer programmatic integration
  • Deterministic call binding — the current Python implementation uses canonical JSON key ordering to produce stable SHA-256 call digests; future cross-language/native signed evidence will use a standardized canonicalization scheme (RFC 8785 / JCS) rather than relying on Python-specific JSON serialization
  • Replay prevention — consumed authorizations cannot be reused for a second dispatch
  • Post-dispatch resilience — if a provider fails to record execution evidence, the local record is preserved

Core invariant

ToolBoundary remains the local enforcement authority. An external provider can add a stricter gate or external evidence, but it can never turn a local deny into an allow.

Why this exists

Enterprise AI-governance platforms (agent registries, policy engines, approval dashboards) make sense when a large organization has dozens of AI agents built by different teams and needs a compliance layer to track all of them. That's real infrastructure for a real problem — but it's disproportionate for the much more common case: one developer or a small team building one to a handful of agents, who just need to make sure a tool-calling agent can't do something catastrophic.

ToolBoundary is built for that second case. It costs nothing, requires no infrastructure, and takes minutes to add to an existing agent.

Quickstart

from toolboundary import Boundary, ToolPermission, AutonomyLevel, AccessMode

boundary = Boundary(
    agent_name="support-agent",
    autonomy=AutonomyLevel.LIMITED_AUTONOMOUS,
    permissions=[
        ToolPermission("read_ticket_db", access_mode=AccessMode.READ_ONLY),
        ToolPermission(
            "send_reply_email",
            access_mode=AccessMode.EXECUTE,
            max_calls_per_hour=30,
        ),
    ],
    blocked_operations=frozenset({"delete_ticket"}),
    max_actions_per_hour=100,
    kill_switch_env="TOOLBOUNDARY_KILL_SWITCH",
)

# Somewhere in your agent's tool-calling code:
boundary.check("read_ticket_db", access_mode=AccessMode.READ_ONLY)   # passes silently
boundary.check("delete_ticket", operation="delete_ticket")           # raises BoundaryViolation

If a call is denied, boundary.check(...) raises BoundaryViolation (or a more specific subclass like KillSwitchActive or RateLimitExceeded). If a call needs a human before it can proceed, it raises ApprovalRequired. Every decision — allow, deny, or approval-required — is written to a structured audit log automatically.

Emergency stop

export TOOLBOUNDARY_KILL_SWITCH=1

Set the environment variable your Boundary was configured with, and every future call for that agent is denied immediately — no restart required, no code change, no separate dashboard to log into.

Two ways to enforce the boundary

1. Decorator (plain Python functions)

from toolboundary import guarded_tool, AccessMode

@guarded_tool(boundary, access_mode=AccessMode.EXECUTE, value_arg="amount")
def wire_transfer(account_id: str, amount: float) -> str:
    return f"transferred {amount} to {account_id}"

wire_transfer(account_id="acct_1", amount=250_000)
# raises BoundaryViolation if 250_000 exceeds the permission's max_value —
# the function body never executes.

Once a function is decorated, calling it is calling through ToolBoundary. There is no code path to the real implementation that skips the check.

2. LangChain tools

from toolboundary.integrations.langchain import guard_tools
from toolboundary import AccessMode

guarded_tools = guard_tools(
    [read_db_tool, send_email_tool, wire_transfer_tool],
    boundary,
    default_access_mode=AccessMode.READ_ONLY,
    overrides={
        "send_email_tool": {"access_mode": AccessMode.EXECUTE},
        "wire_transfer_tool": {"access_mode": AccessMode.EXECUTE, "value_arg": "amount"},
    },
)

agent_executor = AgentExecutor(agent=agent, tools=guarded_tools)

This wraps the LangChain BaseTool objects themselves — the objects your AgentExecutor actually invokes when the LLM decides to call a tool — so the boundary check runs inside LangChain's own tool-execution path, not as a step the agent's reasoning loop has to remember to call.

Install with the LangChain extra: pip install toolboundary[langchain]

External authorization providers (v1.0.1)

ToolBoundary can optionally consult an external authorization provider before dispatching a tool call. The provider adds a second gate — it can never weaken a local policy decision.

Without a provider (default — unchanged from v0.1.0)

boundary = Boundary(
    agent_name="support-agent",
    ...
)
# Works exactly as before. No external service needed.

With a provider (observe mode)

from toolboundary import Boundary, ProviderMode

boundary = Boundary(
    agent_name="support-agent",
    ...,
    provider=my_provider,
    provider_mode=ProviderMode.OBSERVE,
)
# Provider decisions are logged but don't block locally-allowed actions.
# Provider unavailability is gracefully degraded.

With a provider (enforce mode)

boundary = Boundary(
    agent_name="support-agent",
    ...,
    provider=my_provider,
    provider_mode=ProviderMode.ENFORCE,
)
# Provider must explicitly allow the action.
# Provider denial or unavailability blocks execution before dispatch.

Implementing a custom provider

Any class that implements the EvidenceProvider protocol can serve as a provider:

from toolboundary import EvidenceProvider, FrozenToolCall, LocalDecision, ProviderGrant
from toolboundary import ExecutionRecord, ProviderReceipt

class MyProvider:
    def authorize(self, call: FrozenToolCall, local: LocalDecision) -> ProviderGrant:
        # Your authorization logic here
        return ProviderGrant(allowed=True, provider="my-provider")

    def record(self, grant: ProviderGrant, execution: ExecutionRecord) -> ProviderReceipt:
        # Your evidence recording logic here
        return ProviderReceipt(recorded=True, provider="my-provider")

AgentKey integration

AgentKey is the first external provider being developed against ToolBoundary's provider-neutral contract.

The ToolBoundary repository contains the ToolBoundary-side reference integration, example, and end-to-end test coverage. The AgentKey-specific provider implementation is maintained on the AgentKey side and plugs into the existing EvidenceProvider interface.

from toolboundary import Boundary, ToolPermission, AutonomyLevel, AccessMode, ProviderMode
from toolboundary.integrations.agentkey import AgentKeyProvider

provider = AgentKeyProvider(client=your_agentkey_client)

boundary = Boundary(
    agent_name="support-agent",
    autonomy=AutonomyLevel.AUTONOMOUS,
    permissions=[ToolPermission("read_ticket", access_mode=AccessMode.READ_ONLY)],
    provider=provider,
    provider_mode=ProviderMode.ENFORCE,
)

Key points:

  • AgentKey is optional. The core pip install toolboundary remains independent.
  • ToolBoundary remains the local enforcement authority. A local deny is final.
  • The provider contract is vendor-neutral. Other authorization/evidence providers can implement the same interface.
  • Cross-language canonical evidence is not defined by Python json.dumps(). The native signed-evidence work will use RFC 8785 (JCS) as the canonicalization target.
  • The reference integration is intentionally small. It covers pre-dispatch authorization and post-dispatch evidence without moving security authority out of ToolBoundary.

See examples/agentkey_integration.py for the ToolBoundary-side example and tests/test_agentkey_integration.py for the integration coverage.

Authorization flow

Local policy check → DENY? → stop (provider never consulted)
                   → ALLOW? → freeze call → compute digest → provider.authorize()
                                                            → execute exact call
                                                            → provider.record()

What a Boundary can enforce

Control Example
Which tools an agent may use at all permissions=[ToolPermission("read_db", ...)]
Operation-level allow/block lists blocked_operations=frozenset({"delete_customer"})
Access mode (READ_ONLY / WRITE / EXECUTE / ADMIN) access_mode=AccessMode.EXECUTE
Transaction value ceilings ToolPermission(..., max_value=500_000)
Record-count ceilings ToolPermission(..., max_records=100)
Rate limits (global or per-tool) max_actions_per_hour=60
Autonomy level AutonomyLevel.RECOMMEND_ONLY / HUMAN_APPROVAL_REQUIRED / LIMITED_AUTONOMOUS / AUTONOMOUS / QUARANTINED
Time-bounded validity valid_from=, valid_to=
Environment restriction allowed_environments=frozenset({"DEV", "TEST"})
Emergency kill switch in-process flag or environment variable
Custom policy logic policy_hooks=[my_custom_check]
External authorization provider provider=my_provider, provider_mode=ProviderMode.ENFORCE
Exact-call binding with cryptographic digest Automatic when a provider is configured

Full field reference: see docs/API.md.

Audit trail

Every decision produces a structured event. By default it goes to Python's standard logging module under the logger name toolboundary.audit, so it flows into whatever logging pipeline you already have (stdout, a file, CloudWatch, Datadog, etc.) with zero extra code.

from toolboundary.audit import AuditTrail, JSONLFileSink

boundary = Boundary(
    agent_name="support-agent",
    ...,
    audit=AuditTrail(sinks=[JSONLFileSink("toolboundary-audit.jsonl")]),
)

For native or cross-language verifiable evidence, the canonicalization and signing format must be specified independently of Python's JSON serializer. See the RFC 8785/JCS evidence work planned for the native evidence roadmap.

When a provider is configured, audit events automatically include evidence metadata: call digests, provider decisions, authorization IDs, and result digests.

A WebhookSink is also included if you want to forward events to a self-hosted dashboard or a centralized governance platform. Audit delivery is always best-effort — a network hiccup in your audit pipeline can never block or crash your agent, because the ALLOW/DENY decision has already been enforced locally before the sink is invoked.

Design philosophy

  • Fail closed. Anything ambiguous, misconfigured, or erroring is treated as denied by default. See fail_closed_on_hook_error for the one place this is configurable.
  • No infrastructure required. No database, no server, no login. The whole thing is a Python object you construct alongside your agent code.
  • Version-controlled policy. Your boundary is code, reviewed in the same pull requests as everything else — not a setting buried in a web UI that drifts silently out of sync with what the agent actually does.
  • Loud by default. Denials raise exceptions, not silent False returns that are easy to accidentally ignore.
  • Framework-agnostic core, framework-specific adapters. The core Boundary has zero dependencies. Framework integrations (LangChain today; more welcome via PR) are optional extras.
  • Local authority, optional extension. External providers add evidence and stricter gates but never override local policy. ToolBoundary works identically with or without a provider configured.

Known limitations — please read this

ToolBoundary is an in-process, application-layer library. Being explicit about what it does not do is more important than what it does:

  • It cannot stop an agent that bypasses it entirely. If your agent's code has any path that calls a tool's real implementation directly — instead of through a @guarded_tool-wrapped function or a guard_tool-wrapped LangChain tool — that call is not evaluated. ToolBoundary governs the doors you route through it; it is not a network firewall.
  • It is not a substitute for credential scoping. If the underlying API key or database credential your tool uses has broader permissions than ToolBoundary's policy allows, a determined attacker who obtains that credential directly bypasses ToolBoundary entirely. Scope your actual credentials as tightly as you can — ToolBoundary is a second layer, not a replacement for the first.
  • It is not a compliance/audit system of record for large organizations. If you have dozens of agents across multiple teams and need human governance workflows, cross-team registries, and formal approval routing, look at enterprise AI governance platforms — ToolBoundary is intentionally not trying to be that.
  • The in-memory rate limiter is per-process. If you run multiple replicas of your agent, each process has its own rate-limit counters unless you supply a shared backing store (see Boundary's internals / open an issue if you need this — a Redis-backed limiter is a natural community contribution).
  • Provider evidence is only as trustworthy as the provider. An SDK-reported result does not itself prove that an external side effect occurred — it proves the SDK reported it. See the provider documentation for what guarantees each provider makes.

If your threat model requires guaranteeing that a compromised agent physically cannot reach a tool's network endpoint except through an approved path, you need a network-layer control (a sidecar proxy, egress firewall rule, or service mesh policy) in addition to ToolBoundary, not instead of it.

Installation

pip install toolboundary                # core, zero dependencies
pip install toolboundary[langchain]     # + LangChain integration

Contributing

Issues and PRs are welcome. See CONTRIBUTING.md.

Ideas that would make great first contributions:

  • Custom EvidenceProvider implementations for popular platforms
  • Redis-backed rate limiter for multi-process deployments
  • CrewAI / AutoGen / LangGraph integrations (mirroring integrations/langchain.py)
  • A minimal read-only local dashboard that tails a JSONLFileSink log

License

MIT — see LICENSE.

Metadata

Release files for toolboundary 1.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for toolboundary 1.0.1
File Size Uploaded
toolboundary-1.0.1.tar.gz 63.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for toolboundary 1.0.1
File Interpreter ABI Platform
toolboundary-1.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 106.7 kB

Release files / toolboundary-1.0.1.tar.gz

Download URL toolboundary-1.0.1.tar.gz
Size 63.6 kB
Tags Source
SHA-256 checksum
How to use checksums
1a61691214db45e11b38670ff11a8ef13b60b474cf7822459b5b855186acab50
BLAKE2b-256 checksum
How to use checksums
a6f79feccf4f0a81dafc1951972fcd686b75d6f5302ee3ae0f042279aa700bd3
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 Oct 4, 2026.

Transparency log

Release files / toolboundary-1.0.1-py3-none-any.whl

Download URL toolboundary-1.0.1-py3-none-any.whl
Size 43.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
902cbfecb9a5145a31233ae51de398fc2959c3fad05ff8049221b8fcd6509d32
BLAKE2b-256 checksum
How to use checksums
feeec4600b08ca6d4ea551549cd9a1e943e3bae69bdb1f4c949bcdbfa58de998
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 Oct 4, 2026.

Transparency log

Release history Release notifications | RSS feed

1.0.2

2 release files

This release

1.0.1 This release

2 release files

1.0.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page