Metadata-Version: 2.5
Name: ccs-pydantic-ai
Version: 0.1.1
Summary: CCS (Capability Compliance System) runtime receipts for Pydantic AI agents.
Project-URL: Homepage, https://github.com/correctover/ccs-integrations
Project-URL: Documentation, https://github.com/correctover/ccs-integrations/tree/main/adapters/ccs-pydantic-ai#readme
Project-URL: Issues, https://github.com/correctover/ccs-integrations/issues
Author: Correctover / CCS Integrations
License-Expression: MIT
License-File: LICENSE
Keywords: agent,attestation,ccs,compliance,ed25519,pydantic-ai,receipt,verification
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Pydantic
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: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: cryptography>=41.0.0
Requires-Dist: jcs>=0.2.0
Requires-Dist: pydantic-ai>=0.1.0
Provides-Extra: dev
Requires-Dist: ccs-verifier==1.3.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Provides-Extra: verify
Requires-Dist: ccs-verifier==1.3.0; extra == 'verify'
Description-Content-Type: text/markdown

# ccs-pydantic-ai

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
[![Conformance](https://img.shields.io/badge/CCS-v1.3.0%20vectors%20%E2%9C%93-success)](#conformance)

> The first official framework integration for **CCS (Capability Compliance
> System)**. Add cryptographically-verifiable runtime receipts to every tool
> call in a [Pydantic AI](https://ai.pydantic.dev/) agent with **2 lines of
> code**.

Every tool call emits a signed, 30-field **L1 action receipt** plus a linked
`ccs.behavior_evidence.v1` receipt. Both verify independently with the
closed-core `ccs-verifier==1.3.0` package, and the L1 to behavior link is bound
by a SHA-256 digest over JCS-canonical JSON.

```python
from ccs_pydantic_ai import CCSCapability, CCSConfig
from pydantic_ai import Agent

agent = Agent(
    "openai:gpt-4o",
    capabilities=[CCSCapability(CCSConfig(seed=b"my-app-seed"))],
)
```

No changes to your tools, agent structure, or prompts are required.

---

## Installation

```bash
pip install ccs-pydantic-ai

# Optional: add the closed-core verifier to independently validate receipts:
pip install "ccs-pydantic-ai[verify]"   # pulls in ccs-verifier==1.3.0
```

The adapter itself is **MIT licensed**. The core `ccs-verifier` dependency is
**ELv2** and is only needed for *verifying* receipts, not for producing them.

## Quick start

### Recommended: `CCSCapability` (covers local + MCP tools)

```python
import asyncio
from pydantic_ai import Agent
from ccs_pydantic_ai import CCSCapability, CCSConfig

def search(query: str) -> str:
    """Search the knowledge base."""
    return f"Results for {query!r}"

async def main():
    agent = Agent(
        "openai:gpt-4o",
        tools=[search],
        capabilities=[CCSCapability(CCSConfig(
            deployment_mode="in-process",
            seed=b"my-app-seed",
            issuer="my-app/ccs",
            audience="my-audience",
        ))],
    )
    result = await agent.run("Search for X")
    print(result.output)
    # Receipts are printed to stdout as JSON lines by default.

asyncio.run(main())
```

`CCSCapability` wraps the agent's *assembled combined toolset* each run via
`AbstractCapability.get_wrapper_toolset(...)`, so **every tool -- local function
tools and MCP tools alike -- is intercepted**. This is the interception point
confirmed by a Pydantic AI maintainer in
[pydantic/pydantic-ai#4262](https://github.com/pydantic/pydantic-ai/issues/4262).

### Explicit: wrap a single toolset

If you only want receipts for a specific toolset (e.g. one MCP server), wrap it
directly:

```python
from pydantic_ai import FunctionToolset
from ccs_pydantic_ai import CCSToolset, CCSConfig

my_tools = FunctionToolset(tools=[search, calculate])
agent = Agent(
    "openai:gpt-4o",
    toolsets=[CCSToolset(my_tools, CCSConfig(seed=b"my-app-seed"))],
)
```

### Run the bundled example

```bash
pip install -e ".[dev]"
python examples/basic_agent.py   # uses TestModel -- no API key needed
```

## Configuration

```python
CCSConfig(
    deployment_mode="in-process",     # "in-process" | "sidecar"
    seed=b"my-app-seed",              # required for in-process key derivation
    # sidecar_url="http://localhost:9100",  # sidecar signing endpoint
    # public_key="...base64 Ed25519...",     # trusted key for sidecar mode
    # signer=my_custom_signer,              # override with any CCSSigner
    rule_version="1.3.0",
    rule_summary="no_rules_matched",
    issuer="my-app/ccs",
    audience="my-audience",
    trace_id=None,                     # fixed trace id; auto per run if None
    receipt_ttl_seconds=300.0,
    max_clock_skew=0.0,
    verifier_source_class="PydanticAIAdapter",
    sink=my_callable,                  # ReceiptRecord -> None; default stdout
    include_behavior_receipts=True,
    action_suffix="execute",           # action field becomes "<tool>.execute"
)
```

### Deployment modes

| Mode | Private key location | Reproducible | Forgeable on process compromise |
|------|----------------------|--------------|---------------------------------|
| `in-process` | Inside the agent process (derived from `seed` via `Ed25519PrivateKey.from_private_bytes(sha256(seed))`) | Yes | Yes |
| `sidecar` | Outside the process (held by the CCS sidecar) | No | No -- only the public key is embedded |

In **sidecar mode** the adapter never holds the private key. It POSTs the
canonical payload to `{sidecar_url}/sign`, receives a base64 Ed25519 signature,
and **verifies that signature locally against the configured public key** before
attaching it to the receipt.

### Receipt sink

By default each receipt pair is printed to stdout as one JSON line. Pass any
callable `(ReceiptRecord) -> None`:

```python
receipts = []
config = CCSConfig(seed=b"x", sink=receipts.append)
```

`ReceiptRecord` exposes `.l1` (dict), `.behavior` (dict or `None`),
`.trace_id`, `.tool_call_id`, and `.verdict`.

## Verifying receipts

```python
from ccs_verifier.ccs_verifier_l1 import L1Receipt
from ccs_pydantic_ai import linked_l1_digest, verify_ed25519

# L1: strict parse (rejects unknown/tampered fields) + signature
l1 = L1Receipt.from_dict(record["l1"], strict=True)
assert l1.verify_signature() is True

# Behavior evidence: signature + linkage to the L1 receipt
beh = record["behavior"]
assert verify_ed25519(beh["public_key"], beh, beh["signature"])
assert beh["linked_l1_receipt_digest"] == linked_l1_digest(record["l1"])
```

If any L1 field is modified after signing, `verify_signature()` returns `False`
and the behavior receipt's `linked_l1_receipt_digest` no longer matches.

## Architecture

```
Tool call (local fn or MCP)
        |
        v
+----------------------+    Ed25519 over JCS (RFC 8785)
|   CCSToolset         |    (private key never leaves the signer)
|   .call_tool()       |
|                      |
|  (1) record request  |
|  (2) invoke tool     |
|  (3) record response |
|  (4) build L1 (30)   |----> sign ----> L1 receipt
|  (5) build behavior  |----> sign ----> behavior receipt
|  (6) emit to sink    |
+----------------------+
        |
        v
   ReceiptSink (stdout / callback / file / queue ...)
```

* **Hashing** -- `args_digest`, `params_hash`, `request_hash`, `response_hash`,
  `runtime_context_hash`, and `config_hash` are all SHA-256 over JCS-canonical
  JSON.
* **Signing** -- Ed25519; the `signature` field is excluded from the signed
  payload, while `signing_algorithm` and `public_key_fingerprint` are included
  to prevent algorithm/key substitution.
* **Linkage** -- `linked_l1_receipt_digest = "sha256:" + sha256(JCS(L1 minus
  signature))`, matching the CCS v1.3.1 paired conformance vectors.

## Receipt structure

### L1 action receipt (30 fields)

```
trace_id, receipt_version ("1.1"), verdict ("allow"|"block"), timestamp,
tool, tool_call_id, params_hash, args_digest, rule_summary, rule_version,
request_hash, response_hash, runtime_context_hash, config_hash,
verifier_source_class, deployment_mode, issuer, audience,
nonce, sequence, issued_at, expires_at, max_clock_skew, action,
signature, signing_algorithm ("Ed25519"),
public_key_fingerprint, public_key, verified_at, latency_us
```

### Behavior evidence receipt (`ccs.behavior_evidence.v1`)

```
receipt_type, trace_id, tool_call_id, sequence,
linked_l1_receipt_digest, behavior_evidence_verdict
  ("not_observed" | "observed_and_rejected" | "observed_and_allowed"),
evidence_ref, issuer, audience, issued_at, deployment_mode,
signing_algorithm, public_key_fingerprint, public_key, signature
```

## Conformance

The adapter's key derivation, JCS canonicalization, field set, and linkage
algorithm are verified against the published CCS conformance vectors
(v1.3.0 / v1.3.1):

* In-process seed `b"ccs-verifier/in-process-test/v1"` reproduces the vector
  public key `6PPlM1taN/Ws4SnxaypgY2CGcKvGPw/eC54cUNesSb8=`
  (fingerprint `bbca301d8848dfdb`).
* All generated L1 receipts pass `L1Receipt.from_dict(data, strict=True)` and
  `verify_signature()` against `ccs-verifier==1.3.0`.
* Behavior receipts are signed and linked exactly as the independent
  `verify_v131.py` conformance checker requires.

## Development

```bash
cd ccs-integrations/adapters/ccs-pydantic-ai
pip install -e ".[dev]"
pytest
```

The test suite covers:

* **signer** -- deterministic key derivation, cross-key rejection, tamper
  detection, JCS canonicalization, sidecar signature verification.
* **receipt chain** -- strict 30-field L1 parse through `ccs-verifier`,
  Ed25519 verification, behavior linkage, per-field tamper detection, sequence
  ordering.
* **toolset** -- allow/block verdicts through real Pydantic AI agents
  (`TestModel`, no API key), `CCSToolset` and `CCSCapability` integration,
  per-run trace isolation, config validation.

## Security notes

* The adapter never modifies `ccs-verifier` or the 30-field L1 structure.
* In sidecar mode the private key is never present in the agent process; the
  adapter only stores the trusted public key.
* Receipt generation failures are logged to stderr and **never break the agent
  run** -- the tool result/exception is always propagated unchanged.
* For production in-process deployments, supply a high-entropy `seed` via a
  secret manager; treat it as a signing key (process compromise enables
  forgery, per the CCS trust model).

## License

MIT -- see [LICENSE](LICENSE). The adapter depends on `ccs-verifier` (ELv2) only
as an optional `[verify]` extra.
