Metadata-Version: 2.4
Name: agent-action-capsule
Version: 0.5.0
Summary: Reference emit + verify library for the Agent Action Capsule SCITT profile (draft-mih-scitt-agent-action-capsule). Includes the rungs 1+2+6 CORE: emit, record, anchor client, self-attest.
Author: Action State Group
License-Expression: BSD-3-Clause
Project-URL: Homepage, https://github.com/action-state-group/agent-action-capsule
Project-URL: Source, https://github.com/action-state-group/agent-action-capsule
Project-URL: Draft, https://datatracker.ietf.org/doc/draft-mih-scitt-agent-action-capsule/
Keywords: scitt,cose,agent,capsule,verification,transparency
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security :: Cryptography
Classifier: Development Status :: 3 - Alpha
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: ruff<0.16,>=0.4; extra == "dev"
Requires-Dist: langchain-core>=0.1; extra == "dev"
Requires-Dist: scitt-cose>=0.1.1; extra == "dev"
Provides-Extra: envelope
Requires-Dist: scitt-cose>=0.1.1; extra == "envelope"
Provides-Extra: transparent
Requires-Dist: scitt-cose>=0.1.1; extra == "transparent"
Provides-Extra: anchor
Requires-Dist: scitt-cose>=0.1.1; extra == "anchor"
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.1; extra == "langchain"
Provides-Extra: crewai
Requires-Dist: crewai>=0.40; extra == "crewai"
Dynamic: license-file

# agent-action-capsule (reference library)

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](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.

## Quickstart (30 seconds)

Install the payload-only core (stdlib only — **zero required dependencies**):

```bash
pip install agent-action-capsule        # from a checkout: cd python && pip install .
```

**Verify a conformant capsule** — point at a shipped positive vector:

```bash
$ 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:

```bash
$ 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).

### The two layers

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`](https://github.com/action-state-group/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.

```python
from agent_action_capsule.producer_envelope import verify_producer_envelope
```

To verify both layers of a SCITT **Signed Statement**, install the optional extra
and pass `--transparent`:

```bash
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.

### Build → verify (producer side)

[`../examples/build_and_verify.py`](../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.

### Conformance vectors

[`../vectors/capsule/`](../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`](../spec/section-map.md) for the spec section map.

## What it implements

| 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). |

```python
from agent_action_capsule import verify, Capsule, EffectRecord, Disposition, AssuranceBlock

capsule = Capsule(
    spec_version="draft-mih-scitt-agent-action-capsule-04", 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
```

### Cross-record references (`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:

```python
from agent_action_capsule import Capsule, ReferenceEntry, LogCoordinates

capsule = Capsule(
    spec_version="draft-mih-scitt-agent-action-capsule-04", 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.

## Scope boundary (deliberate)

**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** —
- **Substrate verification.** The COSE_Sign1 signature, registration, and the
  Receipt's inclusion proof are the SCITT/COSE substrate's, by reference
  ([`scitt-cose`](https://github.com/action-state-group/scitt-cose)). This
  package never derives `anchored`; a claimed `anchored` mode is reported as an
  unverifiable overclaim (§5.3).
- **Class 2 / manifest-aware verification** (§8.2). Constraint Records are
  *represented* as data (§8.1) but no manifest is fetched and no evidence-schema
  check is performed.

## Develop / test

```bash
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.
