Metadata-Version: 2.4
Name: agentoracle-receipt-verify
Version: 0.1.0
Summary: Verifier for AgentOracle composed envelopes (RFC 8785 JCS + Ed25519). Canonicalization is byte-identical to the production Node canonicalizer.
Author-email: AgentOracle <joe@agentoracle.co>
License: MIT
Project-URL: Homepage, https://agentoracle.co
Project-URL: Repository, https://github.com/TKCollective/agentoracle-receipt-verify-py
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=42.0.0
Dynamic: license-file

# agentoracle-receipt-verify

Verifier for AgentOracle composed envelopes: RFC 8785 JCS canonicalization plus Ed25519 JWS signature verification. Canonicalization output is byte-identical to the production Node canonicalizer.

## Design goal

Three implementations, one canonicalization. A receipt canonicalized in Node, Python, or the browser must produce the byte-identical string and byte-identical SHA-256. No language-specific behavior. No trusted issuer round-trip.

## Install

```bash
pip install agentoracle-receipt-verify
```

## Usage

**Key material is required to reach a verdict.** Pass `jwks_by_issuer`:

```python
from agentoracle_receipt_verify import verify

result = verify(envelope, jwks_by_issuer={
    "https://agentoracle.co/.well-known/jwks.json": ao_jwks,
    "https://agenttrust.uk/.well-known/jwks.json": at_jwks,
})

if result.status == "valid":
    print("verified — canonical:", result.canonical_sha256)
```

### Three outcomes, not two

| `status` | `valid` | Meaning |
|---|---|---|
| `"valid"` | `True` | Every check ran and passed |
| `"invalid"` | `False` | A check ran and failed; see `.errors` |
| `"indeterminate"` | `None` | A check could not run; see `.indeterminate_reason` |

**Calling `verify(envelope)` without `jwks_by_issuer` on a signed envelope returns `indeterminate`, not `valid`.** Canonicalization recompute proves the payload matches its claimed hash; it binds the payload to no issuer. Only the signature does that. A verifier that reported `valid` there would assert a property it never tested.

`None` is falsy, so `if result.valid:` fails closed. Branch on `.status` when you need to distinguish "failed" from "could not check".

## What it checks

| Invariant | Description |
|---|---|
| `canonical_recomputes` | JCS(payload) → SHA-256 recomputes byte-identical to claimed |
| `decision_ref_recomputes` | `sha256(JCS(preimage))` matches published `decision_ref` (per invinoveritas/babyblueviper1 spec) |
| `decision_signer_ne_runtime` | Decision signer issuer ≠ runtime issuer (fail-closed: self-approval is void) |
| `all_signatures_verified` | Every JWS signature verifies against a resolvable JWK by `kid`. `None` when no key material was supplied — unevaluated, not failed |

## Cross-language guarantees

The `tests/` suite includes byte-identical fixtures shared with the Node reference implementation:

- `test_jcs_byte_identical_to_node` — Python JCS output byte-matches Node output for a payload with nested objects, arrays, unicode, booleans, and integers.
- `test_decision_ref_recompute_babyblueviper1` — Python recomputes the shipped [invinoveritas fixture](https://github.com/babyblueviper1/preaction-governance-conformance/tree/3e54ee2/examples/decision-ref-recompute), byte-identical to her Python and our Node.
- `test_conformance_sample_canonical_hash` — reproduces the canonical hash from AgentOracle's `/v1/conformance/sample` production endpoint.

## License

MIT
