Metadata-Version: 2.4
Name: keel-ai
Version: 0.1.0
Summary: Python SDK for Keel — agent-governance compliance middleware
Project-URL: Homepage, https://github.com/cooperdunkin/keel
Project-URL: Repository, https://github.com/cooperdunkin/keel
Project-URL: Issues, https://github.com/cooperdunkin/keel/issues
Author-email: Cooper Dunkin <cooperdunkin228@gmail.com>
License: Apache-2.0
License-File: LICENSE
Keywords: agent,ai,compliance,governance,mcp
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2.6
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == 'mcp'
Description-Content-Type: text/markdown

# keel (Python SDK)

[![PyPI](https://img.shields.io/pypi/v/keel-ai.svg)](https://pypi.org/project/keel-ai/)
[![Python versions](https://img.shields.io/pypi/pyversions/keel-ai.svg)](https://pypi.org/project/keel-ai/)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)

Python SDK for [Keel](https://github.com/cooperdunkin/keel) — agent-governance
compliance middleware for regulated finance. Wrap your agent's tool calls so
every action is evaluated against policy before it runs.

Async-first (`httpx` + `asyncio`), fully typed (Pydantic v2, `mypy --strict`).

## Installation

```bash
pip install keel-ai          # core SDK (KeelClient + guard)
pip install keel-ai[mcp]     # adds MCP handler (create_mcp_tool_handler)
```

## KeelClient

```python
import asyncio
from keel import KeelClient, KeelClientOptions, Policy, EvaluationContext, Action

policy = Policy(
    id="treasury-cap",
    name="Treasury per-transaction cap",
    version=1,
    default_action="allow",
    rules=[
        {
            "id": "block-over-50k",
            "action": "block",
            "reason": "Over per-transaction limit.",
            "condition": {
                "type": "spend_limit_per_transaction",
                "currency": "USD",
                "max_amount": 50_000,
            },
        }
    ],
)

async def main() -> None:
    client = KeelClient(KeelClientOptions(base_url="https://api.keel.dev"))
    result = await client.evaluate(
        policy,
        EvaluationContext(
            agent_id="treasury-agent",
            correlation_id="corr-1",
            timestamp="2026-06-01T12:00:00Z",
            action=Action(type="transaction", amount=10_000, currency="USD"),
        ),
    )
    print(result.decision)  # "allow"

asyncio.run(main())
```

## guard()

Wrap async tool handlers so each call is enforced against the policy. A `block`
raises `KeelGuardBlockedError` (the handler never runs); an `escalate` raises
`KeelGuardEscalatedError`; an `allow` runs the original handler.

```python
from keel import KeelClient, KeelClientOptions, guard, GuardOptions, Action
from keel import KeelGuardBlockedError

client = KeelClient(KeelClientOptions(base_url="https://api.keel.dev"))

async def wire_transfer(args: dict) -> str:
    return f"wired {args['amount']} to {args['counterparty']}"

guarded = guard(
    GuardOptions(
        client=client,
        policy=policy,
        agent_id="treasury-agent",
        tool_to_action=lambda name, args: Action(
            type="transaction",
            amount=args.get("amount"),
            currency=args.get("currency"),
        ),
    ),
    {"wire_transfer": wire_transfer},
)

async def run() -> None:
    try:
        out = await guarded["wire_transfer"]({"amount": 60_000, "currency": "USD"})
        print(out)
    except KeelGuardBlockedError as err:
        print(f"blocked: {err.reason} (policy {err.policy_id}, rule {err.rule_id})")
```

## Using with MCP

If you expose your tools over the [Model Context Protocol](https://modelcontextprotocol.io),
`create_mcp_tool_handler` wraps an MCP `tools/call` handler so every invocation
is enforced by `guard()` and guard outcomes become MCP-shaped `CallToolResult`s
with `isError=True` (block / escalate / governance-unreachable / unknown tool) —
so the model can self-correct instead of the action slipping through.

This requires the optional `mcp` extra:

```bash
pip install keel[mcp]
```

```python
from keel import KeelClient, KeelClientOptions, Action
from keel.mcp import McpGuardOptions, McpToolHandlerOptions, create_mcp_tool_handler
from mcp.types import CallToolRequest, CallToolResult, TextContent

client = KeelClient(KeelClientOptions(base_url="https://api.keel.dev"))

async def wire_transfer(args: dict) -> CallToolResult:
    return CallToolResult(
        content=[TextContent(type="text", text=f"wired {args['amount']}")],
    )

handle = create_mcp_tool_handler(
    McpToolHandlerOptions(
        guard=McpGuardOptions(
            client=client,
            policy=policy,
            agent_id="treasury-agent",
            tool_to_action=lambda name, args: Action(
                type="transaction", amount=args.get("amount"), currency=args.get("currency")
            ),
        ),
        tools={"wire_transfer": wire_transfer},
    )
)

# Dispatch an MCP tools/call request through Keel:
result = await handle(
    CallToolRequest(
        method="tools/call",
        params={"name": "wire_transfer", "arguments": {"amount": 60_000, "currency": "USD"}},
    )
)
# result.isError is True when the policy blocked/escalated the call.
```

## Fail-open

Set `failure_mode="open"` on a low-risk `Policy` (serialized as `failureMode`)
so that, if the Keel API is unreachable (`KeelNetworkError` /
`KeelEvaluateTimeoutError`), the guard runs the tool unguarded and logs a
`guard_fail_open` line to stderr instead of failing closed. Default is `closed`.
