Metadata-Version: 2.5
Name: tool-call-guard
Version: 0.2.0
Summary: Deny-by-default policy gate for AI agent tool calls: allowlists, argument validation, rate caps, human-approval hooks, dry-run mode, and an audit trail. Framework-agnostic, zero dependencies.
Project-URL: Homepage, https://github.com/binaydhakal/tool-call-guard
Project-URL: Repository, https://github.com/binaydhakal/tool-call-guard
Project-URL: Issues, https://github.com/binaydhakal/tool-call-guard/issues
Author-email: Binaya Dhakal <binaydhakal35@gmail.com>
License: MIT
License-File: LICENSE
Keywords: agents,ai,allowlist,audit,guardrails,human-in-the-loop,llm,mcp,policy,rate-limit,security,tool-calling
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Security
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: anthropic
Requires-Dist: claude-agent-sdk<0.3,>=0.2.139; extra == 'anthropic'
Provides-Extra: integrations
Requires-Dist: claude-agent-sdk<0.3,>=0.2.139; extra == 'integrations'
Requires-Dist: openai-agents<0.21,>=0.20.0; extra == 'integrations'
Provides-Extra: openai
Requires-Dist: openai-agents<0.21,>=0.20.0; extra == 'openai'
Description-Content-Type: text/markdown

# tool-call-guard (Python)

[![CI](https://github.com/binaydhakal/tool-call-guard/actions/workflows/ci.yml/badge.svg)](https://github.com/binaydhakal/tool-call-guard/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/tool-call-guard)](https://pypi.org/project/tool-call-guard/)
[![license](https://img.shields.io/pypi/l/tool-call-guard)](https://github.com/binaydhakal/tool-call-guard/blob/main/LICENSE)

**Deny-by-default policy gate for AI agent tool calls.** The Python half of [tool-call-guard](https://github.com/binaydhakal/tool-call-guard) — same JSON policy model and audit schema as the [npm package](https://www.npmjs.com/package/@yanib/tool-call-guard), so one security review covers both stacks.

```python
from tool_call_guard import Guard, ToolCallDenied

guard = Guard(
    {
        "defaultAction": "deny",              # anything unlisted is blocked
        "tools": {
            "search_*": {},                   # allowlist a group
            "send_email": {
                "validate": lambda a: a["to"].endswith("@mycompany.com")
                or "external recipients need approval",
                "maxCallsPerMinute": 5,
            },
            "deploy": {"action": "approve"},  # human-in-the-loop
            "shell_exec": {"action": "deny"},
        },
    },
    approve=lambda req: ask_operator(req),    # sync here; async via acheck()
)

@guard.protect("send_email")
def send_email(args):
    ...

send_email({"to": "attacker@evil.com"})       # raises ToolCallDenied
```

## Install

```sh
pip install tool-call-guard
```

The core has zero dependencies and supports Python 3.9+. Provider SDKs are optional extras and currently require Python 3.10+ through their upstream packages. Validators accept plain callables (return `True`/`False`/reason-string, or raise) or pydantic-style model classes (anything with `model_validate`).

## Provider adapters

### OpenAI Agents SDK

```sh
pip install "tool-call-guard[openai]"
```

Attach the policy adapter to an OpenAI function tool's input guardrails. A denied call never reaches the function; by default the adapter returns a safe rejection message to the model.

```python
from agents import function_tool
from tool_call_guard import Guard
from tool_call_guard.integrations.openai_agents import create_tool_input_guardrail

guard = Guard({
    "tools": {
        "search": {},
        "shell": {"action": "deny"},
    }
})

@function_tool(tool_input_guardrails=[create_tool_input_guardrail(guard)])
def search(query: str) -> str:
    """Search internal documents."""
    return search_documents(query)
```

Set `denied_behavior="raise_exception"` to trip the run instead of returning model-visible rejection content. The default rejection text is generic; use the `message` option when the model should receive a curated reason. Invalid JSON arguments fail closed and are never copied into the adapter response.

### Anthropic Claude Agent SDK

```sh
pip install "tool-call-guard[anthropic]"
```

Register the adapter as a `PreToolUse` hook:

```python
from claude_agent_sdk import ClaudeAgentOptions
from tool_call_guard import Guard
from tool_call_guard.integrations.claude_agent_sdk import create_hook_matcher

guard = Guard({
    "tools": {
        "Read": {},
        "mcp__docs__*": {},
        "Bash": {"action": "deny"},
    }
})

options = ClaudeAgentOptions(
    hooks={"PreToolUse": [create_hook_matcher(guard)]},
)
```

Allowed calls return no permission decision, so the SDK's native permission checks still run. Denied calls return a structured `PreToolUse` denial with generic text unless you set the `message` option. With `mode="dry-run"`, the hook records `would_allow` without changing the SDK's permission flow.

## What the policy gives you

- **Deny-by-default** — unlisted tools are blocked; the allowlist is the policy.
- **Wildcard rules** — `"fs_*"` budgets and gates a whole group; exact names beat patterns.
- **Argument validation** — runs before quota, so malformed calls never consume budget.
- **Quotas** — `maxCalls` per guard lifetime, `maxCallsPerMinute` sliding window (injectable clock).
- **Approval hooks** — `action: "approve"` calls your approver; no approver configured means deny, not allow.
- **Dry-run mode** — everything proceeds, but the audit trail records what enforcement *would* have done. Observe a policy in production before turning it on. Approvers are never invoked during a rehearsal.
- **Audit trail** — in-memory ring buffer plus optional sinks; `jsonl_audit(path)` writes one JSON line per decision, same schema as the JS package.

## API sketch

```python
guard = Guard(policy, mode="enforce"|"dry-run", approve=..., on_audit=...,
              audit_args=True, max_audit_events=1000, now=time.time)

guard.check(tool, args)   -> Decision      # sync; sync approvers only
await guard.acheck(tool, args)             # async; sync or async approvers
guard.wrap(name, fn)                       # sync fn -> sync wrapper, async -> async
@guard.protect(name)                       # decorator form
guard.wrap_tools({name: fn, ...})
guard.audit_log                            # ring buffer, newest last
guard.reset()
```

`Decision`: `allowed`, `action`, `reason`, `tool`, `rule`, and in dry-run `would_allow` + `dry_run`. Denied wrapped calls raise `ToolCallDenied` (with `.decision`).

Policy keys are camelCase (portable JSON, shared with the JS package); snake_case aliases (`max_calls`, …) are accepted in Python.

See the [repository root](https://github.com/binaydhakal/tool-call-guard) for the full policy reference and the threat model this addresses.

## License

MIT © [Binaya Dhakal](https://www.dhakalbinaya.com.np)
