ccs-pydantic-ai
The first official framework integration for CCS (Capability Compliance System). Add cryptographically-verifiable runtime receipts to every tool call in a Pydantic AI 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.
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
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)
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.
Explicit: wrap a single toolset
If you only want receipts for a specific toolset (e.g. one MCP server), wrap it directly:
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
pip install -e ".[dev]"
python examples/basic_agent.py # uses TestModel -- no API key needed
Configuration
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:
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
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, andconfig_hashare all SHA-256 over JCS-canonical JSON. - Signing -- Ed25519; the
signaturefield is excluded from the signed payload, whilesigning_algorithmandpublic_key_fingerprintare 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 key6PPlM1taN/Ws4SnxaypgY2CGcKvGPw/eC54cUNesSb8=(fingerprintbbca301d8848dfdb). - All generated L1 receipts pass
L1Receipt.from_dict(data, strict=True)andverify_signature()againstccs-verifier==1.3.0. - Behavior receipts are signed and linked exactly as the independent
verify_v131.pyconformance checker requires.
Development
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),CCSToolsetandCCSCapabilityintegration, per-run trace isolation, config validation.
Security notes
- The adapter never modifies
ccs-verifieror 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
seedvia a secret manager; treat it as a signing key (process compromise enables forgery, per the CCS trust model).
License
MIT -- see LICENSE. The adapter depends on ccs-verifier (ELv2) only
as an optional [verify] extra.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ccs_pydantic_ai-0.1.1.tar.gz.
File metadata
- Download URL: ccs_pydantic_ai-0.1.1.tar.gz
- Upload date:
- Size: 25.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
703fc14ef4f9868c689283a410cd319c5ba6526e9e9610f21cf25dc10ad1f157
|
|
| MD5 |
c8be1aca82b66f18a3560287f21bb7cb
|
|
| BLAKE2b-256 |
657e22a5453bafaa9b7a4ea51a350ccadffa1f992222a2e88671ff064f1557cd
|
File details
Details for the file ccs_pydantic_ai-0.1.1-py3-none-any.whl.
File metadata
- Download URL: ccs_pydantic_ai-0.1.1-py3-none-any.whl
- Upload date:
- Size: 25.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a55adc28186bcd8f6b647024f6295b95bdafc88ccc26fafac0e786f6ba932313
|
|
| MD5 |
989e28cc7754fb2d09c0f502449e5a8e
|
|
| BLAKE2b-256 |
b6fabd82046dd0cd476cdaf9e05df1b625bae4ca1a379f4f7237243e3667179d
|