Observed since 20 Oct, 21:27 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
agent-action-capsule
Reference verifier for the Agent Action Capsule SCITT profile (draft-mih-scitt-agent-action-capsule), with the typed Capsule carriers and their sealing boundary Capsule.seal(), and the anchor client.
The reference implementation of the Agent Action Capsule profile: parse and
seal a Capsule, and run the Class 1 verifier defined in the
Internet-Draft (../spec/). Published to PyPI as agent-action-capsule.
See RELEASING.md for the release workflow and PyPI Trusted
Publisher setup.
The spec (../spec/) is the source of truth. Where the draft says MUST / MUST
NOT, the code and tests enforce it, section by section.
Install the payload-only core (stdlib only — zero required dependencies):
pip install agent-action-capsule # from a checkout: cd python && pip install .
Verify a conformant capsule — point at a shipped positive vector:
$ agent-action-capsule verify ../vectors/capsule/pos-executed-confirmed/input.json
Agent Action Capsule — Class-1 payload verification: ../vectors/capsule/pos-executed-confirmed/input.json
ok: True
capsule_id (recomputed): 5b7c4ff1bcf4364e0dd8c8b65eddddbb1d6102f27bed3ad1a3595cba51d3502a
derived: effect_mode=confirmed attestation_mode=self_attested ledger_mode=standalone
findings: none
# exit code 0
Verify a failing capsule — the finding is self-explaining, with its §6 check number:
$ agent-action-capsule verify ../vectors/capsule/neg-confirmed-without-response/input.json
ok: False
...
findings:
- [error] (check 3) confirmed_without_response: effect.status 'confirmed' requires a 64-hex response_digest (§5.2)
# exit code 1 (0 = ok, 1 = not ok, 2 = could not run / malformed input)
Add --json for the raw VerificationResult, or --store <dir-or-ledger-file>
for store-level chain checks (supersedes / concurrent-supersedes / open-items).
Verification is two layers, and this package is honest about which it does:
| Layer | Who verifies it | This package |
|---|---|---|
| payload — the Agent Action Capsule (Class-1, §6): capsule_id, confirmed-effect binding, effect_attestation matrix, disposition, assurance | this profile | ✅ implemented |
| producer authentication — exact three-header Producer Envelope over the raw Capsule ID | this profile | ✅ implemented by producer_envelope.py |
| transparency substrate — distinct RFC 9943 registration statement plus Receipt / inclusion proof | SCITT/COSE, by reference | calls scitt-cose — never reimplemented |
The current AAC Producer Envelope is separate from an RFC 9943 Signed Statement:
it signs the raw 32-byte Capsule ID with exactly three protected headers and
does not carry RFC 9943's required CWT claims. Install
agent-action-capsule[envelope] and call
verify_producer_envelope(capsule_id, envelope_bytes) to verify that profile.
The function returns the authenticated public key; it does not decide whether
the key is authorized for an operator or developer.
from agent_action_capsule.producer_envelope import verify_producer_envelope
To verify an Evidence Bundle's interval coverage and per-record membership
(agent_action_capsule.bundle.verify_bundle), install the bundle extra. It
adds the public CLL reference that checks the MMR proofs:
pip install 'agent-action-capsule[bundle]'
Without it those two claims fail closed as completeness_verifier_unavailable.
That finding means this installation could not check the proofs. It does not
mean the certificate is invalid.
verify_bundle(bundle, strict=True) adds the checks a verifier needs before it
calls a Bundle VALID, and sets result.verdict to VALID, INCOMPLETE or
INVALID:
signatures), including vintage jcs-n
records; an unsigned record is not shown unless you pass
require_signature=True;checkpoint); without
one, interval coverage and membership are not shown, so the Bundle is never
VALID;disclosures overlay as one claim, and
whether every record names the checkpoint's key;checkpoint.witnesses (cll, rekor and scrapi
bindings) under the keys of the witness_directory you pass, a parsed
witnesses.json. Without one, no witness is trusted.Strict mode needs agent-action-capsule[bundle,envelope].
vectors/bundle/strict/ states the rules.
build_evidence_bundle(source, root=..., interval_from=...) builds a Bundle
from a ProofSource: the log's signed COSE checkpoint, its leaves, records by
Capsule ID, and inclusion and range proofs. It holds no key, and it checks the
result with verify_bundle(strict=True) before returning it, refusing anything
that verifier calls INVALID. proof_source_from_cll(backend, records) makes a
ProofSource from a CLL backend (scan_entries, load_cll) and a record
store whose get(capsule_id) returns capsule and producer_envelope bytes.
interval_from=N carries every record from seq N to the checkpoint tip, and
none below N, with a range proof: the Bundle can read VALID. N=1 is the
whole log.interval_from=None carries only the chosen records, each with its own
inclusion proof, and states completeness.selection: "producer-selected":
nothing is claimed about the records between them, so it reads INCOMPLETE.interval_from has no default, so a caller never hands over the whole log by
omission. A range over a leaf that is not a record, a carried record below N
and a record past the checkpoint are refused by name. A missing citation or
an unsigned record is stated, not refused, and reads INCOMPLETE.
build_unproven_bundle(records, root=...) is the form with no log proof, for
Capsules that belong to no log. build_evidence_bundle needs the bundle and
envelope extras; build_unproven_bundle needs only envelope.
agent_action_capsule.settlement.verify_settlements(capsules, key_policy=..., wrapped_objects=...) derives the payment and delivery states of
draft-mih-agent-settlement-records-00 from both parties' leg records. It
passes every case in vectors/settlement/. It needs
the envelope extra.
To answer an Evidence Request, call
agent_action_capsule.evidence_request.answer(request_bytes, store=..., signer=...).
It implements draft-mih-agent-evidence-request-00 and passes every case in
vectors/evidence-request/: it parses the
request (JSON, or deterministic CBOR with binding="cbor"), resolves the
subject exactly, and returns an Artifact or a signed Refusal. Your store
(an EvidenceStore) answers exact lookups, lists its coverage anchors and
builds the artifact, so the responder works over any log. To send an
Artifact in the interop profile's signed envelope, pass it to sign_envelope
and encode the result in the request's binding. check_refusal and
verify_refusal_offline check a signed refusal with no network. A requester
reads a reply with parse_reply, authenticates an envelope with
Envelope.verify_for(request_bytes, responder_key), and checks its
Checkpointed Local Log material with check_cll_material; a responder encodes
that material with encode_cll_inclusion_proof and
encode_cll_consistency_proof. The module needs
cryptography only to sign refusals and envelopes and to verify signatures,
so install the envelope extra for that.
To verify both layers of a SCITT Signed Statement, install the optional extra
and pass --transparent:
pip install 'agent-action-capsule[transparent]' # adds the public scitt-cose verifier
agent-action-capsule verify --transparent statement.cose --issuer-key issuer_pub.pem [--log-key log_pub.pem --leaf-entry-hex <hex>]
For an RFC 9943 Capsule-ID registration statement, this command authenticates and prints the raw Capsule ID. Class-1 verification of the Capsule JSON is a separate operation. For a JSON-bearing Signed Statement, it also runs Class-1 verification over the authenticated payload.
substrate (SCITT/COSE, via scitt-cose):
signature_verified : True
receipt_verified : True
attestation_tier : anchored # 'anchored' ONLY when a receipt actually verified (§3.2)
payload (Agent Action Capsule, Class-1):
ok: True
Without a verified receipt the substrate tier is self_attested, never
anchored. The optional extra is public (scitt-cose, deps cbor2 +
cryptography); the default install pulls nothing extra.
../examples/build_and_verify.py is the
smallest honest round trip: construct a capsule with the typed builders, seal()
it (compute capsule_id), then verify() it — an executed action and a blocked
one, in ~40 lines.
Capsule.seal() is this package's sealing boundary. Recording an agent's
actions as they happen (default dispositions, chaining, framework adapters) is
capsule-emit's job: use
its capsule_emit.seal and its LangChain and CrewAI adapters
(capsule_emit.adapters.langchain, capsule_emit.adapters.crewai).
agent_action_capsule.emit() and agent_action_capsule.integrations (with the
langchain and crewai extras), deprecated in 0.9.0, are removed in 0.10.0. The
version constants (DEFAULT_SPEC_VERSION, ACCEPTED_SPEC_VERSIONS, ...) live
in agent_action_capsule.versions and stay exported from the package root.
../vectors/capsule/ is the frozen conformance suite a second
implementer runs against their own verifier: each input.json plus its
spec-anchored expected.json (ok, the §6 check numbers + severities, the
derived modes, the recomputed capsule_id). See
../spec/section-map.md for the spec section map.
| Module | Spec | Implements |
|---|---|---|
canonical.py | §2, §5.1 | Current JSON-DIGEST uses plain RFC 8785 JCS. Format-4 capsule_id excludes itself and the local-only producer-envelope fields (signature, key_id) — those are attached to the ledger line after the id is computed, so they can never be part of its preimage — while committing the declaration and chain. This reference is format-4-only: any other format_version is rejected with unsupported_format_version, and the legacy absent-field construction has been removed. Vintage format-2 records are verified with the frozen legacy-verify/v0.1.0 release, not this reference. |
producer_envelope.py | §3, §6 | Optional exact-profile COSE_Sign1 verification over the raw 32-byte Capsule ID. Returns the authenticated Ed25519 key; caller authorization remains separate. |
registries.py | §12 + Disclosure Envelope §4 | Loads the seven registries (incl. citation_purpose, draft-04) from ../spec/REGISTRY.md and exports the companion disclosure-eligibility table. |
disclosure_envelope.py | Disclosure Envelope DE-1–DE-3 | Runs Class 1 over the embedded Capsule independently, then validates eligibility, committed-digest presence, and disclosure JSON-DIGEST equality. |
contracts.py | §5.2–§5.4, §5.5.5 | Typed producer carriers whose constructors enforce the invariants a producer MUST NOT violate: the disposition honesty invariant and the closed approver enum (§5.4), the confirmed-effect binding and the status/digest table (§5.2), and references[] entry structure / AAC self-identity digest format (§5.5.5). A non-conforming Capsule cannot be built. Also the effect_mode derivation (§5.2) and the never-dispatch set (§5.4.2). |
verify.py | §6, §5.5.5 | The Class 1 verifier: the eight checks in fixed order plus references[] findings (§5.5.5, spliced into checks 1/6/8), a structured result that never throws, a single ok boolean, store-level chain checks (verify_store), and the SHOULD-level defensive disposition-honesty assert over arbitrary bytes. Unknown registry values are informational, never a rejection. |
parse.py | §5, §5.5.5 | Capsule builder + seal() (computes capsule_id); strict parse_capsule (raises on a non-conforming Capsule). references[] is tri-state: absent, present-and-empty, and present-and-populated are three distinct wire forms (and therefore three distinct capsule_id digests). |
from agent_action_capsule import verify, Capsule, EffectRecord, Disposition, AssuranceBlock
capsule = Capsule(
spec_version="draft-mih-scitt-agent-action-capsule-05", format_version="4",
canonicalization_id="jcs",
action_id="po-12345", action_type="decide", operator="ACME-CO", developer="agent@v1",
timestamp="2026-06-13T00:00:00Z",
effect=EffectRecord(status="confirmed", type="write_order",
response_digest="a"*64, effect_attestation="gate_executed"),
assurance=AssuranceBlock(attestation_mode="self_attested", effect_mode="confirmed",
ledger_mode="standalone"),
disposition=Disposition(decision="accept", approver="human", human_disposed=True,
verdict_class="executed"),
).seal()
result = verify(capsule) # never throws
assert result.ok # a single `ok` gates trust in every other field
references[], §5.5.5)chain is exclusively a Capsule's own same-stream parent. references cites
everything else — a different producer's Capsule, or any other artifact this
action targeted or responded to. Absent and an explicit empty array are
semantically the same ("no such citation") but are distinct bytes, so they
produce different capsule_id digests — pass references=() only when that
distinction matters to a caller:
from agent_action_capsule import Capsule, ReferenceEntry, LogCoordinates
capsule = Capsule(
spec_version="draft-mih-scitt-agent-action-capsule-05", format_version="4",
canonicalization_id="jcs",
action_id="deny-42", action_type="decide", operator="ACME-CO", developer="agent@v1",
timestamp="2026-09-08T00:00:00Z",
references=(
ReferenceEntry(
type="agent-action-capsule", digest_alg="SHA-256",
digest="b" * 64, # the cited Capsule's capsule_id
citation_purpose="responds_to", # or "acted_on"; §12 citation_purpose registry
),
ReferenceEntry(
type="foreign-artifact", digest_alg="future-hash", digest="opaque-digest",
log_coordinates=LogCoordinates(log_id="example-log", leaf_index=3, inclusion_proof={...}),
),
),
).seal()
capsule["references"] # [{"type": ..., "digest_alg": ..., "digest": ..., "citation_purpose": ...}, {...}]
A references entry MUST NOT duplicate chain.parent_capsule_id — citing the
producer's own same-stream parent is stated once, in chain, never
redundantly in references (Capsule(...) raises InvariantError if it
does). Only the agent-action-capsule/SHA-256 self-identity context
constrains digest to 64-lowercase-hex; every other type/digest_alg
combination is CPB's to define and stays open. log_coordinates, when
present, is an upgrade (not a second identity) and its inclusion_proof is
never independently verified by Class 1.
Does — the Class 1 agent-profile surface (§6), performable from the Capsule's own bytes plus the registry contents (and, for chain checks, the producer's store of Capsules).
Does NOT —
scitt-cose). This
package never derives anchored; a claimed anchored mode is reported as an
unverifiable overclaim (§5.3).cd python
pip install -e ".[dev]"
python -m pytest -q # positive + negative (MUST-reject) suite
python -m ruff check .
The test suite is the conformance contract: every MUST / MUST NOT in the
implemented sections has a positive and a negative case, and the frozen
byte-level vectors under ../vectors/capsule/ are replayed through verify() /
verify_store(). The two-layer (--transparent) tests run only when the
optional [transparent] extra is installed, and skip cleanly otherwise.
Download counts and popularity will appear here as the collector receives them.
| Package / target | Requirement | Relationship |
|---|---|---|
pytestEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest>=7.0; extra == "dev" | runtime |
ruffEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.9",
"evaluation": "NOT_EVALUATED"
} | ruff<0.16,>=0.4; extra == "dev" | runtime |
scitt-coseEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.9",
"evaluation": "NOT_EVALUATED"
} | scitt-cose>=0.1.1; extra == "dev" | runtime |
rfc8785Environment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.9",
"evaluation": "NOT_EVALUATED"
} | rfc8785==0.1.4; extra == "dev" | runtime |
cryptographyEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.9",
"evaluation": "NOT_EVALUATED"
} | cryptography>=42; extra == "dev" | runtime |
scitt-coseEnvironment{
"extras": "",
"marker": "extra == \"envelope\"",
"requires_python": ">=3.9",
"evaluation": "NOT_EVALUATED"
} | scitt-cose>=0.1.1; extra == "envelope" | runtime |
scitt-coseEnvironment{
"extras": "",
"marker": "extra == \"transparent\"",
"requires_python": ">=3.9",
"evaluation": "NOT_EVALUATED"
} | scitt-cose>=0.1.1; extra == "transparent" | runtime |
scitt-coseEnvironment{
"extras": "",
"marker": "extra == \"anchor\"",
"requires_python": ">=3.9",
"evaluation": "NOT_EVALUATED"
} | scitt-cose>=0.1.1; extra == "anchor" | runtime |
checkpointed-local-logEnvironment{
"extras": "",
"marker": "extra == \"bundle\"",
"requires_python": ">=3.9",
"evaluation": "NOT_EVALUATED"
} | checkpointed-local-log>=0.6.0; extra == "bundle" | runtime |
bdist_wheel · 181.46 KB · Python py3
sdist · 353.87 KB · Python source
Shared repository data · GitHub
Observed since 20 Oct, 21:27 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:05cbb70301ad0dcd626c0fe6f73ff599afe4851e4a5dd69beeb2b29adb9046fc