Metadata-Version: 2.4
Name: scopeblind-pydantic-ai
Version: 0.1.0
Summary: Ed25519 signed receipts + Cedar policy enforcement for Pydantic AI agents
Project-URL: Homepage, https://veritasacta.com
Project-URL: Repository, https://github.com/ScopeBlind/scopeblind-gateway
Project-URL: Bug Tracker, https://github.com/ScopeBlind/scopeblind-gateway/issues
Project-URL: Documentation, https://veritasacta.com
Author-email: Tom Farley <tommy@scopeblind.com>
License: MIT
License-File: LICENSE
Keywords: agent,audit,cedar,ed25519,governance,pydantic-ai,receipts
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
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 :: Security
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.10
Requires-Dist: pynacl>=1.5.0
Provides-Extra: dev
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pydantic-ai>=1.0.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.3; extra == 'dev'
Provides-Extra: pydantic-ai
Requires-Dist: pydantic-ai>=1.0.0; extra == 'pydantic-ai'
Description-Content-Type: text/markdown

# scopeblind-pydantic-ai

[![PyPI version](https://img.shields.io/pypi/v/scopeblind-pydantic-ai)](https://pypi.org/project/scopeblind-pydantic-ai/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue)](https://www.python.org/)

Ed25519 signed receipts and (optional) Cedar policy enforcement for [Pydantic AI](https://github.com/pydantic/pydantic-ai) agents. Every tool call produces a cryptographic receipt that anyone can verify offline with `npx @veritasacta/verify`.

## Why

Pydantic AI agents call tools. Most teams log those calls to stdout, a SaaS dashboard, or nothing at all. When an auditor or regulator asks *"prove what the agent did on April 12"*, logs aren't proof — they're mutable text files held by the operator.

This package wraps every tool call as a Pydantic AI [`Capability`](https://pydantic.dev/docs/ai/capabilities/). The capability:

1. Evaluates a policy (optional — Cedar, custom function, or allow-all)
2. Signs the decision with Ed25519 (RFC 8032)
3. Canonicalizes with JCS (RFC 8785) so the signature verifies deterministically across implementations
4. Hash-chains each receipt to the previous one so insertion/deletion is detectable
5. Exports to JSONL compatible with `@veritasacta/verify` — no ScopeBlind account, no network call, works air-gapped

## Install

```bash
pip install scopeblind-pydantic-ai
```

## Quick start

```python
from pydantic_ai import Agent
from scopeblind_pydantic_ai import ScopeBlindSigning, ReceiptSigner

# Generate or load an Ed25519 signing key
signer = ReceiptSigner.generate()
signer.save_key("./keys/agent.json")

# Attach the capability to your agent
signing = ScopeBlindSigning(signer=signer, agent_name="weather-bot")

agent = Agent(
    "anthropic:claude-sonnet-4-6",
    capabilities=[signing],
    tools=[get_weather, get_forecast],
)

result = await agent.run("What's the weather in Sydney?")

# Inspect and export receipts
print(f"{signing.receipt_count} tool calls signed")
print(f"Public key for verification: {signing.public_key_hex}")
signing.export_receipts("./receipts.jsonl")
```

Verify the output:

```bash
npx @veritasacta/verify receipts.jsonl --key $(jq -r .public_key keys/agent.json)
```

Exit `0` = all receipts valid. Exit `1` = tampered. Exit `2` = malformed.

## With a policy

The `policy` argument is a callable `(tool_name, args) -> (allowed, deny_reason)`:

```python
def no_production_writes(tool_name: str, args: dict) -> tuple[bool, str | None]:
    if tool_name == "database_write" and args.get("env") == "production":
        return (False, "writes to production require human approval")
    return (True, None)

signing = ScopeBlindSigning(signer=signer, policy=no_production_writes)
```

Denied calls raise `SkipToolExecution`, and a `decision="deny"` receipt is still signed — the record survives the block.

### With Cedar

For declarative policy, run Cedar evaluation in your policy callable. Two options:

**Option 1 — use [protect-mcp](https://www.npmjs.com/package/protect-mcp)'s Cedar evaluator** (shells out to the CLI):

```python
import subprocess, json

def cedar_gate(tool_name: str, args: dict) -> tuple[bool, str | None]:
    result = subprocess.run(
        ["npx", "protect-mcp", "evaluate",
         "--policy", "./protect.cedar",
         "--tool", tool_name,
         "--input", json.dumps(args)],
        capture_output=True,
    )
    if result.returncode == 0:
        return (True, None)
    return (False, result.stderr.decode().strip() or "Cedar deny")
```

**Option 2 — use the Cedar WASM bindings** from [cedar-policy/cedar-for-agents](https://github.com/cedar-policy/cedar-for-agents) (merged [#64](https://github.com/cedar-policy/cedar-for-agents/pull/64)) through a Python binding. Native Python Cedar bindings are on the way.

## Receipt format

Every receipt is a JSON object following [IETF draft-farley-acta-signed-receipts](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/):

```json
{
  "payload": {
    "type": "protectmcp:decision",
    "spec": "draft-farley-acta-signed-receipts-01",
    "tool_name": "get_weather",
    "tool_input_hash": "sha256:a3f8...",
    "decision": "allow",
    "issued_at": "2026-04-15T10:30:00.000Z",
    "issuer_id": "scopeblind-pydantic-ai",
    "session_id": "sess_3a8b2c",
    "sequence": 1,
    "previousReceiptHash": null,
    "agent_name": "weather-bot",
    "output_hash": "sha256:b7e2..."
  },
  "signature": {
    "alg": "EdDSA",
    "kid": "sb:pydai:bd84c36b620c",
    "sig": "4cde814b..."
  }
}
```

The signature covers the JCS-canonicalized payload, so any edit to any payload field breaks verification.

## Ecosystem

Cross-compatible with the rest of the ScopeBlind / Veritas Acta stack. A receipt signed by this package verifies identically to one signed by:

- [`protect-mcp`](https://www.npmjs.com/package/protect-mcp) (Node, Claude Code hooks)
- [`protect-mcp-adk`](https://pypi.org/project/protect-mcp-adk/) (Google ADK)
- The Rust gateway (APS ProxyGateway)
- The Microsoft Agent Governance Toolkit adapter ([PR #667](https://github.com/microsoft/agent-governance-toolkit/pull/667), merged)

## Compatibility

| Pydantic AI | Status |
|---|---|
| `>= 1.0.0` | Supported — uses the `Capability` API (`wrap_tool_execute` hook) |
| `< 1.0` | Not supported — the hook API landed in the 1.0 release |

## Development

```bash
git clone https://github.com/ScopeBlind/scopeblind-gateway
cd scopeblind-gateway/packages/scopeblind-pydantic-ai
pip install -e ".[dev]"
pytest tests/ -v
```

## Standards

- **Ed25519** — [RFC 8032](https://datatracker.ietf.org/doc/html/rfc8032)
- **JCS** — [RFC 8785](https://datatracker.ietf.org/doc/html/rfc8785)
- **Cedar** — [AWS's open authorization engine](https://www.cedarpolicy.com/)
- **IETF Internet-Draft** — [draft-farley-acta-signed-receipts](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/)

## License

MIT. See [LICENSE](./LICENSE).
