English · 中文 · Español · 日本語 · Português
Official Python SDK for the Digital Citizenship Protocol (DCP v2.0). Pydantic v2 models, hybrid post-quantum signatures (Ed25519 + ML-DSA-65, plus SLH-DSA-192f), the ML-KEM-768 KEM primitive, composite signatures with pq_over_classical binding, V1 and V2 bundle signing and verification, OpenTelemetry observability, DCP-04 through DCP-09 behavior, and a full-featured CLI.
Release status. This README describes the source on main. The last tagged release, v2.10.0, has
dcp-ai 2.9.0 for Python. What the changelog lists under [Unreleased] is not in
a tagged release: admission, delegation chains, the A2A receive window, bounded deny-list workers and the
experimental dcp-jcs-v2 APIs. To run the examples below, install from a checkout:
git clone https://github.com/dcp-ai-protocol/dcp-ai.git
pip install -e "dcp-ai/sdks/python"The registry install gives the published version, without the unreleased features:
pip install dcp-aiFrom the registry (published version); from a checkout, the same extras apply: pip install -e "dcp-ai/sdks/python[fastapi]".
pip install "dcp-ai[fastapi]" # FastAPI middleware
pip install "dcp-ai[langchain]" # LangChain integration
pip install "dcp-ai[openai]" # OpenAI wrapper
pip install "dcp-ai[crewai]" # CrewAI multi-agent
pip install "dcp-ai[otlp]" # OpenTelemetry OTLP exporter| Area | Status |
|---|---|
| Ed25519 / ML-DSA-65 (FIPS 204) providers | Yes |
| SLH-DSA-192f provider | Yes, FIPS 205 (pure, empty context), interoperable with the other SDKs |
| ML-KEM-768 | KEM primitive: yes (MlKem768Provider, FIPS 203). DCP-04 X25519 + ML-KEM-768 hybrid key exchange: not yet |
Composite signatures (pq_over_classical) + verification |
Yes |
| Canonical JSON v2 + domain separation | Yes |
| Dual hash (SHA-256 + SHA3-256) + Merkle roots | Yes |
| Bundle signing + verification | V1: yes. V2 (SignedBundleV2): yes, BundleBuilderV2 / sign_bundle_v2 / verify_signed_bundle_v2 |
DCP-04 A2A discovery + handshake + AES-256-GCM session (via cryptography); each message accepted once within the window (§8.4) |
Yes |
| DCP-05 agent lifecycle (commissioning / vitality / decommissioning) | Yes |
| DCP-06 digital succession (testament, memory transfer, ceremony) | Yes |
| DCP-07 dispute resolution + arbitration + jurisprudence | Yes |
| DCP-08 rights + obligations + compliance | Yes |
DCP-09 delegation + verifiable delegation chains (create_sub_mandate, verify_mandate_chain, §3.5) + awareness threshold + principal mirror |
Yes |
Session nonce helpers, security tier engine (compute_security_tier(intent, policy_decision, mandate_chain_tier), spec §14.2), emergency revocation |
Yes |
Lazy PQ checkpoints + PQCheckpointManager |
Yes |
| Blinded RPR, multi-party authorization, algorithm advisory helpers | Yes |
Canonical error codes (38 shared across all SDKs) + detect_wire_format |
Yes |
OpenTelemetry / OTLP exporter (optional [otlp] extra) |
Yes |
Admission: trusted principals and the strict-v1 profile |
Yes, unreleased (see Release status) |
Canonicalization candidate dcp-jcs-v2 (ingress, router, bytes verifier, producers) |
Experimental and opt-in, unreleased; not normative |
A V1 bundle signed with Ed25519. The RPR is signed with the principal's key and the passport with the agent's,
each over the canonical JSON of the object without its signature field; create_audit_entry computes
intent_hash and the prev_hash chain. verify_signed_bundle checks the bundle signature, its hashes and the
chain, not these inner signatures: check those yourself, as below. For V2 bundles, see V2 Bundles.
from datetime import datetime, timezone
from dcp_ai import (
AgentPassport,
AuditEvidence,
BundleBuilder,
Intent,
PolicyDecision,
ResponsiblePrincipalRecord,
generate_keypair,
sign_bundle,
sign_object,
verify_object,
verify_signed_bundle,
)
now = datetime.now(timezone.utc).isoformat()
principal_keys = generate_keypair()
agent_keys = generate_keypair()
# 1. The responsible principal, signed with the principal's key (the object without "signature")
rpr_body = {
"dcp_version": "1.0", "human_id": "human:example-001", "legal_name": "Example Principal",
"entity_type": "natural_person", "jurisdiction": "ES", "liability_mode": "owner_responsible",
"override_rights": True, "issued_at": now, "expires_at": None, "contact": None,
}
rpr = {**rpr_body, "signature": sign_object(rpr_body, principal_keys["secret_key_b64"])}
# 2. The agent passport, signed with the agent's key (the object without "signature")
passport_body = {
"dcp_version": "1.0", "agent_id": "agent:example-001", "public_key": agent_keys["public_key_b64"],
"principal_binding_reference": "human:example-001", "capabilities": ["browse", "api_call"],
"risk_tier": "low", "created_at": now, "status": "active",
}
passport = {**passport_body, "signature": sign_object(passport_body, agent_keys["secret_key_b64"])}
# 3. Intent, policy decision and one audit entry
intent = Intent(
intent_id="intent:example-001", agent_id="agent:example-001", human_id="human:example-001",
timestamp=now, action_type="api_call", target={"channel": "api", "domain": "api.example.com"},
data_classes=["none"], estimated_impact="low", requires_consent=False,
)
policy = PolicyDecision(
intent_id="intent:example-001", decision="approve", risk_score=0.05, reasons=["low-impact call"],
)
bundle = (
BundleBuilder()
.responsible_principal_record(ResponsiblePrincipalRecord(**rpr))
.agent_passport(AgentPassport(**passport))
.intent(intent)
.policy_decision(policy)
.create_audit_entry(
audit_id="audit:example-001", timestamp=now, agent_id="agent:example-001",
human_id="human:example-001", intent_id="intent:example-001", policy_decision="approved",
outcome="call_completed", evidence=AuditEvidence(tool="http", result_ref=None),
)
.build()
)
# 4. Sign with the principal's key and verify against the principal's public key
signed = sign_bundle(bundle, principal_keys["secret_key_b64"])
print(verify_signed_bundle(signed, principal_keys["public_key_b64"])) # {'verified': True}
# 5. The inner signatures, over the same objects without "signature"
print(verify_object(rpr_body, rpr["signature"], principal_keys["public_key_b64"])) # True
print(verify_object(passport_body, passport["signature"], agent_keys["public_key_b64"])) # TrueThe SDK includes a CLI built with Typer. Available as dcp after installation.
# Version
dcp version
# Generate Ed25519 keypair
dcp keygen [out_dir]
# Validate an object against a DCP schema
dcp validate <schema_name> <json_path>
# Validate a complete Citizenship Bundle
dcp validate-bundle <bundle_path>
# Verify a Signed Bundle
dcp verify <signed_path> [public_key_path]
# Compute bundle hash (SHA-256)
dcp bundle-hash <bundle_path>
# Compute Merkle root of audit entries
dcp merkle-root <bundle_path>
# Compute intent_hash
dcp intent-hash-cmd <intent_path>| Function | Signature | Description |
|---|---|---|
generate_keypair() |
() -> dict[str, str] |
Returns {"public_key_b64": ..., "secret_key_b64": ...} |
sign_object(obj, secret_key_b64) |
(Any, str) -> str |
Signs, returns base64 |
verify_object(obj, signature_b64, public_key_b64) |
(Any, str, str) -> bool |
Verifies signature |
canonicalize(obj) |
(Any) -> str |
Deterministic JSON |
public_key_from_secret(secret_key_b64) |
(str) -> str |
Derives public key |
| Function | Signature | Description |
|---|---|---|
hash_object(obj) |
(Any) -> str |
SHA-256 of canonicalized JSON |
merkle_root_from_hex_leaves(leaves) |
(list[str]) -> str | None |
Merkle root |
merkle_root_for_audit_entries(entries) |
(list[Any]) -> str | None |
Merkle root of audit entries |
intent_hash(intent) |
(Any) -> str |
Intent hash |
prev_hash_for_entry(prev_entry) |
(Any) -> str |
Previous entry hash |
| Function | Signature | Description |
|---|---|---|
validate_schema(schema_name, data) |
(str, Any) -> dict |
Returns {"valid": bool, "errors": [...]} |
validate_bundle(bundle) |
(dict) -> dict |
Validates a complete bundle |
# Fragment: rpr, passport, intent, policy and entry are the models built in the Quickstart.
bundle = (
BundleBuilder()
.responsible_principal_record(rpr)
.agent_passport(passport)
.intent(intent)
.policy_decision(policy)
.add_audit_entry(entry) # Manual
.create_audit_entry(...) # Auto-computes hashes
.build() # => CitizenshipBundle
)# Signature (not runnable)
sign_bundle(
bundle: CitizenshipBundle,
secret_key_b64: str,
signer_type: str = "human",
signer_id: str | None = None,
) -> dict[str, Any]# Signature (not runnable)
verify_signed_bundle(
signed_bundle: dict[str, Any],
public_key_b64: str | None = None,
) -> dict[str, Any] # {"verified": bool, "errors": [...]}Verifies: schema, Ed25519 signature, bundle_hash, merkle_root, intent_hash chain, prev_hash chain.
BundleBuilderV2, sign_bundle_v2 and verify_signed_bundle_v2 (in dcp_ai.v2) port the TypeScript SDK's builder-v2.ts, signer-v2.ts and verify-v2.ts. Artifacts are SignedPayload dicts ({"payload", "payload_hash", "composite_sig"}); the bundle signature is a composite signature over the manifest.
# Fragment, inside an async function: session_nonce, the signed artifacts (signed_rpr, signed_passport, signed_intent, signed_policy), audit_events, classical_key and pq_key come from your application.
from dcp_ai.providers import Ed25519Provider, MlDsa65Provider
from dcp_ai.v2 import AlgorithmRegistry, BundleBuilderV2, sign_bundle_v2, verify_signed_bundle_v2
registry = AlgorithmRegistry()
registry.register_signer(Ed25519Provider())
registry.register_signer(MlDsa65Provider())
bundle = (
BundleBuilderV2(session_nonce)
.responsible_principal_record(signed_rpr)
.agent_passport(signed_passport)
.intent(signed_intent)
.policy_decision(signed_policy)
.add_audit_entries(audit_events)
.build()
)
signed = await sign_bundle_v2(bundle, registry, "human", "human-001", classical_key, pq_key)
result = await verify_signed_bundle_v2(signed, registry) # DEFAULT_VERIFIER_POLICY
# {"verified": bool, "errors": [...], "warnings": [...], "details": {...}}The verifier also checks each artifact's own composite signature (the RPR against the principal's binding_keys, the rest against the passport keys) and, in every policy mode, rejects a PQ signature that is present but invalid. The mode is the stricter of the policy mode and the security tier's mode (spec §14.3), and details["resolved_tier"] reports the tier.
A verified bundle is signed by the keys it lists; it does not prove who the principal is. Admission accepts a bundle only when its principal signatures were made with keys of a principal your trust set lists, and optionally under the operator-fixed strict-v1 profile (hybrid signatures, signed audience, a session of at most 300 s, a fresh revocation snapshot).
# Fragment, inside an async function: trust_set comes from operator configuration, signed_bundle from the request, snapshot from your revocation source, registry as in V2 Bundles.
from dcp_ai.v2 import admit_signed_bundle_v2, create_principal_trust_store, resolve_strict_v1_profile
trust = create_principal_trust_store(trust_set) # AdmissionConfigError on a bad trust set
profile = resolve_strict_v1_profile({"profile": "strict-v1", "verifier_id": "api.acme.example"})
result = await admit_signed_bundle_v2(signed_bundle, registry, profile=profile, trust=trust, revocation=snapshot)
# result["verified"], result["principal_trust"], result["profile_compliance"], result["admitted"],
# result["action_authorized"] (always False), result["errors"] [{"code", "message"}]verify_signed_bundle_v2(bundle, registry, policy, now_ms=...)takes an optional clock and reportsdetails["signer_keys"]; without them it behaves as before.evaluate_admission_v2evaluates a verifier result you already have (no cryptography, no I/O) and trusts it;revocation_subjectslists what a revocation source must answer for, anddeny_list_revocation_source(url)reads the reference revocation service's deny list; withworkers=DenyListWorkers(max_workers=...)its download threads are counted and capped until they really end (opt-in; see the FastAPI README).- FastAPI:
DCPVerifyMiddleware(app, require_bundle=True, trusted_principals=..., profile=..., revocation_source=...); seeintegrations/fastapi/README.md. Guide:docs/ADMISSION.md. Spec and codes:spec/profiles/admission.
A root mandate is signed by the human (create_delegation_mandate); the agent holding it may issue a
sub-mandate to another agent (create_sub_mandate), signed with its own keys. verify_mandate_chain checks
the chain root first and offline: signatures back to the human, parent links and depth, sub_delegation,
nested validity windows, attenuation (actions, data_classes and limits only narrow), the security-tier
floor and revocation.
# Fragment, inside an async function: registry as in V2 Bundles; orchestrator_classical, orchestrator_pq, root_mandate, nonce and the key entries come from your delegation setup; intent and policy_decision are V2 payloads.
from dcp_ai.v2 import create_sub_mandate, verify_mandate_chain, compute_security_tier
sub = await create_sub_mandate(
registry, orchestrator_classical, orchestrator_pq, root_mandate,
mandate_id="mandate:sub-1", session_nonce=nonce, agent_id="agent:subagent",
authority_scope=[{"domain": "email", "actions_permitted": ["read"], "data_classes": ["contact_info"]}],
valid_from="2026-03-01T00:00:00Z", valid_until="2026-06-01T00:00:00Z", revocable=True,
)
result = await verify_mandate_chain(
registry, [root_mandate, sub], set(),
{"human_keys": human_key_entries, "agent_keys": {"agent:orchestrator": orchestrator_key_entries}},
)
# {"valid", "errors", "depth", "effective_tier"}; the tier floor can only raise an action's tier.
tier = compute_security_tier(intent, policy_decision, result["effective_tier"] if result["valid"] else None)Not normative and not active: dcp-jcs-v1 stays the only canonicalization profile, and no default, builder,
signer or verifier changes. The modules are not re-exported by dcp_ai.v2; they exist for callers who import
them, in a source checkout (see Release status). A verified candidate bundle grants no trust or admission.
Design and contract: docs/proposals/canonicalization-candidate
(§15 ingress, §16 router, §17 verifier, §18 producers).
| Module | API |
|---|---|
dcp_ai.v2.experimental_candidate_ingress |
experimental_candidate_ingress(data): CandidateIngressAccept(value, canonical, canonical_sha256) or CandidateIngressReject(reject_class, offset) |
dcp_ai.v2.experimental_candidate_router |
create_router(transport=..., accepted_profiles=..., historical=...); decoders JSON_LOADS_BYTES and JSON_LOADS_STRICT_DECODE, none a default |
dcp_ai.v2.experimental_candidate_verify |
create_verifier(...), then await verifier.verify(data, registry, policy=None, now_ms=None): the route, the verifier entered and its result |
dcp_ai.v2.experimental_candidate_produce |
sign_payload(registry, context, input, keys) and sign_bundle(registry, parts, signer, options), which check and copy at the call and return an awaitable; ExperimentalCandidateProduceError for a rejection of the inputs' domain or form, raised before any signer is called |
import asyncio
from dcp_ai.providers import Ed25519Provider, MlDsa65Provider
from dcp_ai.v2 import AlgorithmRegistry, DCP_CONTEXTS
from dcp_ai.v2.experimental_candidate_ingress import experimental_candidate_ingress
from dcp_ai.v2.experimental_candidate_produce import sign_payload
# The ingress decides on the original bytes: 1.0 is the integer 1, members are sorted.
print(experimental_candidate_ingress(b'{"b":1.0,"a":[]}').canonical) # b'{"a":[],"b":1}'
refused = experimental_candidate_ingress(b'{"a":1,"a":2}')
print(refused.reject_class, refused.offset) # DUPLICATE_KEY 7
async def main():
registry = AlgorithmRegistry()
keys = {}
for name, provider in (("classical", Ed25519Provider()), ("pq", MlDsa65Provider())):
registry.register_signer(provider)
kp = await provider.generate_keypair()
keys[name] = {"kid": kp["kid"], "alg": provider.alg, "secret_key_b64": kp["secret_key_b64"], "public_key_b64": kp["public_key_b64"]}
artifact = await sign_payload(registry, DCP_CONTEXTS["Intent"], {"bytes": b'{"intent_id":"intent:1","risk":1.0}'}, keys)
print(artifact.signed_payload["payload_hash"]) # sha256:... of {"intent_id":"intent:1","risk":1}
asyncio.run(main())All DCP v1 artifacts are available as Pydantic v2 models with automatic validation:
ResponsiblePrincipalRecord, AgentPassport, Intent, IntentTarget, PolicyDecision, AuditEntry, AuditEvidence, CitizenshipBundle, SignedBundle, BundleSignature, SignerInfo, RevocationRecord, HumanConfirmation
V2 models (dcp_ai.v2.models), with the fields of the V2 schemas. Enumerated values (lifecycle states,
termination modes, dispute status and the like) are Literal fields of these models, not separate classes:
- Core V2:
ResponsiblePrincipalRecordV2,BlindedResponsiblePrincipalRecordV2,AgentPassportV2,KeyEntryV2,IntentV2,PolicyDecisionV2,AuditEventV2,PQCheckpoint,BundleManifest,VerifierPolicy,DcpCapabilities - DCP-05 — Lifecycle:
CommissioningCertificate,VitalityMetrics,VitalityReport,DecommissioningRecord - DCP-06 — Succession:
DigitalTestament,SuccessorPreference,SuccessionRecord,MemoryTransferManifest,MemoryTransferEntry,DualHashRef - DCP-07 — Disputes:
DisputeRecord,ArbitrationResolution,JurisprudenceBundle,ObjectionRecord - DCP-08 — Rights:
RightsDeclaration,RightEntry,ObligationRecord,RightsViolationReport - DCP-09 — Delegation:
DelegationMandate,SubMandate,SubDelegationPolicy,AuthorityScopeEntry,AdvisoryDeclaration,PrincipalMirror,InteractionRecord,AwarenessThreshold,ThresholdRule
# Example: DCP-05 commissioning, signed with a composite (Ed25519 + ML-DSA-65) signature
import asyncio
from dcp_ai.providers import Ed25519Provider, MlDsa65Provider
from dcp_ai.v2 import (
AlgorithmRegistry, CompositeKeyInfo, create_commissioning_certificate, generate_session_nonce,
validate_state_transition,
)
from dcp_ai.v2.models import CommissioningCertificate
async def main():
registry = AlgorithmRegistry()
keys = []
for provider in (Ed25519Provider(), MlDsa65Provider()):
registry.register_signer(provider)
kp = await provider.generate_keypair()
keys.append(CompositeKeyInfo(kp["kid"], provider.alg, kp["secret_key_b64"], kp["public_key_b64"]))
cert = await create_commissioning_certificate(
registry, *keys,
agent_id="agent:example-001", session_nonce=generate_session_nonce(), human_id="human:example-001",
commissioning_authority="human:example-001", purpose="Customer support triage",
initial_capabilities=["browse", "api_call"], risk_tier="low",
principal_binding_reference="human:example-001",
)
CommissioningCertificate(**cert) # validates the fields
print(validate_state_transition("commissioned", "active")) # True
asyncio.run(main())V2 Domain Separation Contexts: Bundle, Intent, Passport, Revocation, Governance, Lifecycle, Succession, Dispute, Rights, Delegation, Awareness
# Install in development mode
pip install -e ".[dev]"
# Run tests
pytest -v
# Async tests
pytest -v --asyncio-mode=autopynacl— Ed25519 cryptographyjsonschema— JSON Schema validationpydanticv2 — Data modelstyper— CLI frameworkpqcrypto≥ 1.0 — ML-DSA-65, SLH-DSA-192f, ML-KEM-768 (FIPS 204/205/203)cryptography— AES-256-GCM for DCP-04 A2A sessions
Apache-2.0