Skip to content

Python SDK

The Python SDK is byte-for-byte interoperable with Go, TypeScript, and Rust. Python 3.10+. Runtime dependencies are cryptography (Ed25519) and pqcrypto (ML-DSA-65) — both audited, both maintained by mainstream Python crypto teams.

Stability: every primitive on this page is stable in 1.0.0a7. There are no preview / experimental APIs in the snippets below.

Terminal window
pip install ratify-protocol==1.0.0a7

The package on PyPI is ratify-protocol. The module you import is ratify_protocol (PEP 8 naming).

import time
from ratify_protocol import (
PROTOCOL_VERSION,
SCOPE_MEETING_ATTEND, SCOPE_MEETING_SPEAK,
DelegationCert, ProofBundle, VerifyOptions,
generate_agent, generate_challenge, generate_human_root,
issue_delegation, sign_challenge, verify_bundle,
)
# 1. Alice generates her hybrid (Ed25519 + ML-DSA-65) root identity.
alice, alice_priv = generate_human_root()
# 2. Her AI agent generates its own hybrid keypair.
agent, agent_priv = generate_agent("Alice's Scheduler", "custom")
# 3. Alice signs a delegation cert for the agent.
now = int(time.time())
cert = DelegationCert(
cert_id="cert-001",
version=PROTOCOL_VERSION,
issuer_id=alice.id, issuer_pub_key=alice.public_key,
subject_id=agent.id, subject_pub_key=agent.public_key,
scope=[SCOPE_MEETING_ATTEND, SCOPE_MEETING_SPEAK],
issued_at=now,
expires_at=now + 7 * 24 * 3600,
signature=None, # populated in place by issue_delegation
)
issue_delegation(cert, alice_priv)
# 4. Verifier issues a challenge. Agent signs it.
challenge = generate_challenge()
challenge_sig = sign_challenge(challenge, now, agent_priv)
# 5. Agent assembles a proof bundle.
bundle = ProofBundle(
agent_id=agent.id,
agent_pub_key=agent.public_key,
delegations=[cert],
challenge=challenge,
challenge_at=now,
challenge_sig=challenge_sig,
)
# 6. Verifier runs the verifier in a single call.
result = verify_bundle(
bundle,
VerifyOptions(required_scope=SCOPE_MEETING_ATTEND),
)
if result.valid:
print(f"✓ Authorized")
print(f" human_id: {result.human_id}")
print(f" agent_id: {result.agent_id}")
print(f" granted_scope: {result.granted_scope}")
print(f" identity_status: {result.identity_status}")
else:
print(f"✗ Rejected: {result.identity_status}{result.error_reason}")

Real output (IDs are derived from the freshly generated keys, so they’ll differ on every run):

✓ Authorized
human_id: b3ef7456eb0a741008ee97cb30ee4aec
agent_id: 98f087953d2da9d52e78f02300df380d
granted_scope: ['meeting:attend', 'meeting:speak']
identity_status: authorized_agent

Verifier-side: branching on identity_status

Section titled “Verifier-side: branching on identity_status”

verify_bundle always returns a VerifyResult. Inspect result.valid first; if it’s false, the specific failure mode is in result.identity_status (a literal string, matching the byte-for-byte protocol-level enum across all four SDKs):

status = result.identity_status
if status == "authorized_agent":
# ✓ All checks passed. result.granted_scope is the intersection of
# every cert's scope (effective scope across the chain).
pass
elif status == "expired":
# ✗ At least one cert is past its expires_at OR not-yet-valid.
pass
elif status == "revoked":
# ✗ A cert ID in the chain matched a revoked entry from the
# revocation provider (or the legacy is_revoked closure).
pass
elif status == "scope_denied":
# ✗ required_scope was not in the chain's effective (intersected)
# scope, OR an advanced PolicyProvider rejected the bundle.
pass
elif status == "constraint_denied":
# ✗ A first-class constraint (geo/time/amount/rate) evaluated to
# "outside the allowed range".
pass
elif status == "constraint_unverifiable":
# ✗ A constraint required input the caller didn't provide
# (e.g. cert has geo_circle but VerifierContext.current_lat was None).
pass
elif status == "constraint_unknown":
# ✗ A cert declared a constraint type this SDK build doesn't recognize.
pass
elif status == "delegation_not_authorized":
# ✗ An intermediate cert sub-delegated without identity:delegate.
pass
elif status == "invalid":
# ✗ Catch-all for structural / cryptographic failures.
# result.error_reason carries a stable machine-readable prefix
# (e.g. "stale_challenge: challenge is 31 seconds old (max 300)").
pass

The verifier is fail-closed by default — any error path returns valid=False.

Constraints (geo / time / amount / rate / speed)

Section titled “Constraints (geo / time / amount / rate / speed)”

When a delegation declares constraints, supply the runtime context:

from ratify_protocol import VerifierContext
ctx = VerifierContext(
current_lat=37.7749,
current_lon=-122.4194,
)
result = verify_bundle(
bundle,
VerifyOptions(
required_scope="drone:deliver",
context=ctx,
),
)

Unset fields (None) on VerifierContext mean “not supplied” — any geo_circle / geo_polygon / geo_bbox constraint will return constraint_unverifiable if you don’t set both current_lat and current_lon.

The full constraint vocabulary (geo_circle, geo_polygon, geo_bbox, time_window, max_speed_mps, max_amount, max_rate) and VerifierContext field requirements are documented in Constraints.

Agent-to-agent delegation uses the same primitive. Alice grants Agent A meeting:attend plus the identity:delegate privilege; A then issues a sub-delegation to Agent B. Without identity:delegate on A’s grant from Alice, A’s sub-delegation will be rejected with delegation_not_authorized.

from ratify_protocol import SCOPE_IDENTITY_DELEGATE
# Alice → A: meeting:attend + identity:delegate
alice_to_a = DelegationCert(
cert_id="cert-alice-to-a",
version=PROTOCOL_VERSION,
issuer_id=alice.id, issuer_pub_key=alice.public_key,
subject_id=agent_a.id, subject_pub_key=agent_a.public_key,
scope=[SCOPE_MEETING_ATTEND, SCOPE_IDENTITY_DELEGATE],
issued_at=now, expires_at=now + 86400,
signature=None,
)
issue_delegation(alice_to_a, alice_priv)
# A → B: a subset of A's grant
a_to_b = DelegationCert(
cert_id="cert-a-to-b",
version=PROTOCOL_VERSION,
issuer_id=agent_a.id, issuer_pub_key=agent_a.public_key,
subject_id=agent_b.id, subject_pub_key=agent_b.public_key,
scope=[SCOPE_MEETING_ATTEND],
issued_at=now, expires_at=now + 3600,
signature=None,
)
issue_delegation(a_to_b, agent_a_priv)
# Chain order: [leaf, ..., root]. A→B is the leaf, Alice→A is the root.
bundle = ProofBundle(
agent_id=agent_b.id, agent_pub_key=agent_b.public_key,
delegations=[a_to_b, alice_to_a],
challenge=challenge,
challenge_at=challenge_at,
challenge_sig=agent_b_sig,
)
result = verify_bundle(
bundle,
VerifyOptions(required_scope=SCOPE_MEETING_ATTEND),
)

result.granted_scope is the lex-sorted intersection of every cert in the chain. identity:delegate doesn’t propagate to B because A didn’t grant it onward — that’s the intentional behavior.

The provider hooks land on VerifyOptions as Optional[...] fields:

from ratify_protocol import (
VerifyOptions,
RevocationProvider, PolicyProvider, AuditProvider,
ConstraintEvaluator, AnchorResolver,
)
result = verify_bundle(
bundle,
VerifyOptions(
required_scope="meeting:attend",
revocation=my_revocation_provider, # §17.1
policy=my_policy_provider, # §17.2
audit=my_audit_provider, # §17.3
constraint_evaluators=my_ext_registry, # §17.7
anchor_resolver=my_anchor_resolver, # §17.8
policy_verdict=cached_verdict, # §17.6
policy_secret=policy_secret,
context=ctx,
),
)

RevocationProvider, PolicyProvider, AuditProvider, ConstraintEvaluator, and AnchorResolver are typing.Protocol classes — duck-typing applies. Any class implementing the expected method signature works; no inheritance required.

Each hook has fail-closed semantics. See Provider architecture for the full reference.

Terminal window
cd ratify-protocol/sdks/python
pip install -e ".[dev]"
pytest -q

You should see all 59 canonical wire-format fixtures plus the 10 alpha.7 cross-SDK byte-equivalence vectors pass. If any fail, the bytes have drifted from the reference and the SDK is not interoperable — file a bug.