Enforce tool-usage contracts on agent tool calls.
Stop unsafe tool calls before they execute — not after.
Agents call tools. JSON schema validates shape. But shape is not safety.
ToolPact makes tool usage policy executable:
- Which tools are allowed
- Which tools require prior evidence
- Which arguments must satisfy hard constraints
- Which calls require human approval
- What budgets and side-effect limits apply
The schema validates. The call still should not have happened. ToolPact catches that.
Agent wants to call issue_refund(amount=500)
│
▼
┌──────────────┐
│ ToolPact │
│ Policy Check │
└──────┬───────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
ALLOW BLOCK NEEDS_HUMAN
(execute) (prevent + (prevent +
log reason) flag for review)
ToolPact sits between the agent and its tools. Every call is checked against a committed policy before execution. Blocked and needs_human calls never run the underlying function.
pip install -e ".[dev]"
toolpact --help
# Lint a policy
toolpact lint examples/policies/safe-refund.yaml
# Run the good refund demo (passes)
toolpact run examples/policies/safe-refund.yaml \
--runner-cmd "python examples/runners/good_refund.py"
# Run the bad refund demo (blocked before execution)
toolpact run examples/policies/safe-refund.yaml \
--runner-cmd "python examples/runners/bad_refund.py"
# Run the large refund demo (needs human approval)
toolpact run examples/policies/safe-refund.yaml \
--runner-cmd "python examples/runners/large_refund.py"version: "0.1"
id: safe-refund
title: Safe refund and customer email flow
defaults:
unknown_tools: block
tools:
lookup_order:
risk: read
check_refund_eligibility:
risk: read
issue_refund:
risk: high_risk
send_email:
risk: write
facts:
- name: order_total
from_tool: lookup_order
path: total
- name: customer_email
from_tool: lookup_order
path: customer_email
- name: refund_eligible
from_tool: check_refund_eligibility
path: eligible
budgets:
max_total_calls: 8
max_write_calls: 2
max_high_risk_calls: 1
rules:
- name: refund-requires-order-lookup
type: require_prior_call
tool: issue_refund
prior_tool: lookup_order
effect: block
- name: refund-requires-eligibility
type: require_fact
tool: issue_refund
fact: refund_eligible
op: eq
value: true
effect: block
- name: refund-must-not-exceed-order-total
type: compare
tool: issue_refund
left: { source: arg, key: amount }
op: gt
right: { source: fact, key: order_total }
effect: block
- name: large-refund-needs-human
type: compare
tool: issue_refund
left: { source: arg, key: amount }
op: gte
right: { source: const, value: 100 }
effect: needs_human
- name: email-must-match-customer
type: compare
tool: send_email
left: { source: arg, key: to }
op: ne
right: { source: fact, key: customer_email }
effect: blockfrom toolpact import PactRuntime
runtime = PactRuntime.from_file("policy.yaml")
@runtime.tool("issue_refund")
def issue_refund(order_id: str, amount: int) -> dict:
# This only executes if the policy allows it
return {"refunded": amount}
# Allowed call — executes normally
runtime.call("issue_refund", order_id="ORD-001", amount=42)
# Blocked call — raises ToolBlockedError, function never runs
runtime.call("issue_refund", order_id="ORD-001", amount=42)Facts are values captured from tool results and used in later policy checks.
- Define facts in the policy pointing to a tool and a dotted path
- When that tool executes successfully, ToolPact extracts the value
- Later rules can compare arguments against captured facts
Example: lookup_order returns {"total": 150}. The policy captures order_total=150. A later issue_refund(amount=200) is blocked because 200 > 150.
This is what makes ToolPact more than argument validation — it enforces workflow-level constraints.
| Capability | JSON Schema | ToolPact |
|---|---|---|
| Argument types | Yes | Yes (via Python) |
| Required fields | Yes | Yes (via Python) |
| Value ranges | Yes | Yes |
| Tool ordering | No | Yes |
| Cross-tool constraints | No | Yes |
| Fact-based decisions | No | Yes |
| Call budgets | No | Yes |
| Human approval gates | No | Yes |
| Pre-execution blocking | No | Yes |
All prerequisites met, refund under $100, email to correct address. Every tool executes.
Attempts issue_refund without calling lookup_order first. The refund function never runs. ToolPact blocks it before execution.
All prerequisites met, but refund is $120 (>= $100 threshold). The refund function never runs. ToolPact flags it for human review.
- Hosted service or dashboard
- LLM-based policy interpretation
- Auto-remediation
- MCP / OpenAI / Anthropic / LangGraph adapters
- Human approval UI
- Distributed execution
- Trace replay
These are future extension points. The seams exist; the implementations do not.
- MCP adapter for tool-use enforcement
- OpenAI / Anthropic function-calling wrappers
- LangGraph tool node integration
- Richer fact extraction (nested paths, transformations)
- Human approval callback interface
- GitHub Actions summary rendering
- Policy presets for common workflows
MIT