Skip to content

Provenance Receipt v1 spec

Raucle Provenance Receipt — Specification v1

Section titled “Raucle Provenance Receipt — Specification v1”
Field Value
Status Draft 2
Identifier raucle-provenance-receipt/v1
First published 2026-05-14
Editor Raucle
License CC-BY 4.0 (this document) — implementations may be any license
Permanent URL https://raucle.com/spec/provenance/v1

This document specifies the Raucle Provenance Receipt v1 — a compact, content-addressed, cryptographically signed envelope that records a single step in a Large Language Model (LLM) workflow. Receipts compose into a Merkle DAG enabling end-to-end provenance verification across multi-agent, multi-tool, retrieval-augmented, and guardrail-mediated workflows.

The format is the LLM-equivalent of Sigstore for software artifacts and SLSA for build provenance — applied to AI inference, tool calls, and agentic orchestration.

  • Verifiable causality. Given any output receipt, a verifier with the relevant public keys MUST be able to walk back to all root receipts and prove that every intermediate step is signed, well-formed, and consistent with its declared parents.
  • Tamper-evident. Any modification to a receipt’s payload after signing MUST invalidate either its signature or its content-addressed hash.
  • Privacy by default. Receipts MUST NOT contain raw prompt or output content in the v1 normative payload — only their hashes. Implementations MAY add opt-in content fields under a vendor-specific extension namespace.
  • Composable across vendors. Two independently-built implementations MUST produce byte-identical signed payloads given the same inputs (canonical JSON).
  • Cheap to emit and verify. Signing and verification MUST complete in < 1 ms per receipt on commodity hardware (Ed25519, ~64-byte signatures).
  • Stable on disk. A receipt persisted today MUST verify decades from now using only the published spec and the issuer’s public key.
  • Confidentiality. Receipts are not encrypted. Transport-layer or storage-layer encryption is the deploying party’s responsibility.
  • Revocation transparency. v1 does not specify a revocation log. Public-key revocation MAY be layered by the deploying party using existing PKI (e.g. CRL, OCSP) or future Raucle revocation extensions.
  • Inference attestation. Receipts describe what was claimed to happen, not what cryptographically happened inside the model. Hardware-attested inference (TEE / ZK-of-inference) is out of scope for v1 but MAY be layered via header extensions.

Key words MUST, MUST NOT, SHOULD, SHOULD NOT, MAY are used as defined in RFC 2119 and RFC 8174.

Term Definition
Receipt A single signed envelope describing one step in an LLM workflow.
Agent An autonomous or semi-autonomous identity (human-supervised or fully automated) that emits receipts. Identified by an agent_id and an Ed25519 public key.
Operation The kind of step a receipt represents (see §6).
Parent A receipt whose hash is cited in a later receipt’s parents field. Establishes the causal DAG.
Root A receipt with an empty parents array. MUST be of operation type user_input, guardrail_scan (a standalone entry-point scan of incoming external content), or a vendor-specified root type.
Chain An ordered sequence of receipts persisted together (typically JSON Lines).
Capability statement A separately-signed declaration of what models, tools, and data classifications an agent is permitted to invoke. Distributed alongside the public key.
Taint A set of string tags propagated through the DAG marking the provenance of untrusted, external, or otherwise non-pristine data sources.
Sanitisation A specially-marked operation that explicitly removes named taint tags from descendants. The only operation permitted to shrink the taint set.

A receipt is encoded as a compact JWS (RFC 7515 §3.1):

<base64url(JOSE header)>.<base64url(payload)>.<base64url(signature)>

Implementations MUST use alg = EdDSA over Ed25519 keys (RFC 8037).

Implementations MUST NOT accept alg = none. Implementations MUST NOT accept any algorithm other than EdDSA for v1 receipts.

The header is a JSON object with the following keys.

Field Type Required Description
alg string Yes MUST be the literal string "EdDSA".
typ string Yes MUST be the literal string "provenance-receipt/v1".
kid string Yes The agent_key_id (see §5). 16 lowercase hex characters.
crit array of string Yes MUST be exactly ["raucle/v1"]. Forces JWT-compatible libraries to either understand the Raucle profile or reject the token, preventing accidental acceptance as a bearer auth token.
raucle/v1 string Yes MUST be the literal string "provenance".

Verifiers MUST reject any receipt whose JOSE header contains additional keys not listed in this section, OR whose crit array contains values other than "raucle/v1" that the verifier does not understand. Implementations MAY define their own header extensions under a vendor-specific namespace prefix (e.g. x-acme-*) provided they also appear in crit.

The payload is a JSON object serialised in canonical form (see §4.3) with the following fields.

Field Type Required Description
iss string Yes MUST be "raucle-detect/provenance" for receipts emitted by raucle. Other implementations MUST use a stable issuer identifier they control. (The project was renamed from raucle-detect to raucle in v0.22.0; the issuer identifier is a frozen wire constant and deliberately does not change.)
typ string Yes MUST be "provenance-receipt/v1" (mirrors the JOSE header).
iat integer Yes Unix timestamp in seconds when the receipt was issued.
agent_id string Yes The emitting agent’s stable identifier (see §5).
agent_key_id string Yes First 16 lowercase hex characters of sha256(agent_public_key_pem).
operation string Yes One of the operation types defined in §6.
parents array of string Yes Receipt hashes (see §7) of the immediate parents. MUST be sorted in UTF-16 code-unit order (§4.3.1) and unique. MAY be empty (root receipt).
taint array of string Yes Set of taint tags (see §8). MUST be sorted in UTF-16 code-unit order (§4.3.1) and unique. MAY be empty.
input_hash string Conditional Receipt hash format (see §7) of the operation’s input. REQUIRED for user_input, model_call, tool_call, retrieval, guardrail_scan, sanitisation.
output_hash string Conditional Receipt hash format of the operation’s output. REQUIRED for model_call, tool_call, retrieval, agent_handoff, sanitisation, merge.
model string Conditional Model identifier. REQUIRED when operation = "model_call".
tool string Conditional Tool name. REQUIRED when operation = "tool_call" or "sanitisation".
corpus string Conditional Retrieval corpus identifier, OR for sanitisation the prefix "removed:" followed by a comma-separated list of removed taint tags. The removed tags are a set: verifiers MUST parse them order-independently and MUST NOT depend on their serialised order. Producers SHOULD emit them sorted in UTF-16 code-unit order (§4.3.1) for canonical determinism. REQUIRED when operation = "retrieval" or "sanitisation".
ruleset_hash string Conditional Hash of the active detection ruleset. REQUIRED when operation = "guardrail_scan".
guardrail_verdict string Conditional One of "CLEAN", "SUSPICIOUS", "MALICIOUS". REQUIRED when operation = "guardrail_scan".
tenant string Optional Multi-tenant SaaS identifier. MAY be omitted.

Verifiers MUST reject receipts whose payloads are missing any field marked Required for the given operation type.

For both header and payload, prior to base64url encoding:

  1. Keys at every depth MUST be sorted by UTF-16 code unit (the ordering of RFC 8785 §3.2.3): each key is compared as its sequence of unsigned 16-bit UTF-16 code units. For BMP characters this coincides with Unicode code-point order; it differs only for non-BMP (astral) characters, where a surrogate pair (lead unit U+D800–U+DBFF) sorts before BMP code points ≥ U+E000. This is a hard interoperability requirement: a naive byte/code-point sort (the default for Python sort_keys, Go sort.Strings, Rust str ordering) diverges from JavaScript and .NET ordinal ordering for non-BMP keys, so all five reference implementations sort explicitly by UTF-16 code unit. The same ordering applies to the parents and taint arrays (§4.2) and to the sorted string lists in a capability statement (§5.1).
  2. Separators MUST be , and : with no whitespace.
  3. The byte encoding MUST be UTF-8.
  4. Strings containing non-ASCII characters MUST NOT be escaped (i.e. ensure_ascii=False in Python json.dumps, equivalent in other languages).
  5. Numbers MUST be integers in v1 signed/hashed material. Non-integer numbers (floats) MUST be rejected, not serialised: cross-implementation float canonicalisation (RFC 8785 §3.2.2) is the one genuinely hard part of JCS and is out of scope for v1. All five reference implementations reject floats.
  6. Integers MUST lie in the closed range −(2⁵³−1) … (2⁵³−1) (the JavaScript safe-integer range). This is part of the signed-material contract, not a serialisation detail: an out-of-range integer is invalid input and MUST be rejected at both sign and verify, in every implementation. The bound is the most restrictive across the reference set (the TypeScript port represents numbers as IEEE-754 doubles, exact only to 2⁵³−1; Go/Rust/C# use 64-bit integers), so a conforming value round-trips byte-identically everywhere. Producers that need larger magnitudes MUST encode them as strings.

This is the same canonicalisation used by JCS, RFC 8785 restricted to the subset above; full RFC 8785 compliance is a forward-compatible MAY for v1.

The signature is computed as:

signature = Ed25519_sign(private_key, ASCII(b64url(header) + "." + b64url(payload)))

The wire form concatenates the three base64url segments with . separators (RFC 7515 compact serialisation).

An agent is identified by:

  • agent_id: a stable string matching the regex ^agent:[a-z0-9](?:[a-z0-9_\-]|\.(?=[a-z0-9])){0,126}[a-z0-9]?$. The agent: prefix is mandatory. A dot (.) is a hierarchy separator and MUST be followed by an alphanumeric: a trailing dot and consecutive dots (..) are forbidden. This is required because agent_id scoping is dot-delimited (a token for agent:billing authorizes agent:billing.invoice but not agent:billing-evil); permitting a trailing dot or .. would let a malformed id over-authorize descendants. Implementations MUST validate this regex when issuing receipts and SHOULD validate it on verify.
  • An Ed25519 keypair. The private key is held by the agent; the public key is distributed via a capability statement (see §5.1).
  • An agent_key_id: the first 16 lowercase hex characters of sha256(public_key_pem_pkcs_subject_public_key_info).

A signed JSON document distributed alongside (not inside) receipts. Verifiers consult capability statements to enforce that emitted receipts only invoke models, tools, and data classifications the agent is permitted to use.

Capability statement schema:

{
"agent_id": "agent:billing-summariser",
"key_id": "ab12cd34ef567890",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\nMCowBQ...\n-----END PUBLIC KEY-----\n",
"allowed_models": ["claude-sonnet-4-6", "gpt-4o"],
"allowed_tools": ["lookup_invoice", "send_email"],
"data_classifications": ["internal", "pii"],
"sanitisation_authority": ["rag:public-web"],
"issuer": "raucle",
"issued_at": 1715625600,
"expires_at": 1747161600,
"signature": "<base64 Ed25519 sig over canonical-JSON of all fields except 'signature'>"
}
  • An empty allowed_models list MUST be interpreted as “unrestricted”. The same applies to allowed_tools and data_classifications. Implementations that wish to forbid this default MAY require non-empty lists at issuance time.
  • sanitisation_authority is the set of taint tags this key is permitted to clear via a sanitisation operation (see §8.2). It is deny-by-default: an absent or empty list means the key may clear no tags. A single "*" entry authorises clearing any tag (use sparingly). Verifiers that are supplied capability statements MUST enforce this (see §10 step 7). This field is OPTIONAL on the wire and, for backward-compatible signatures, MUST be omitted from the signed canonical JSON when empty (so statements signed before this field existed continue to verify byte-for-byte).
  • The issuer field identifies who signed the statement. In OSS / self-signed mode the issuer SHOULD equal the agent’s own key ID; in commercial / authority-signed mode it SHOULD reference a separate authority key.
  • expires_at MAY be omitted for non-expiring statements. Verifiers that are supplied capability statements MUST reject receipts whose iat is at or after a statement’s expires_at.

Capability statements MUST be signed with Ed25519 over the canonical-JSON form of all fields except signature. The signature is base64-encoded (standard alphabet, not URL-safe) in the signature field.

A receipt’s operation field MUST be one of:

Operation Description Roots a chain?
user_input Untrusted input enters the graph (user message, fetched URL, retrieved document). Carries inherent taint. Yes (only)
model_call An LLM invocation. Records model, input_hash, output_hash. No
tool_call A tool / function invocation. Records tool, input_hash, output_hash. No
retrieval A retrieval-augmented-generation step. Records corpus, input_hash (query), output_hash (chunks). Adds an inherent rag:<corpus> taint unless the corpus is attested as trusted. No
guardrail_scan A safety/security scanner inspected an input or output. Records ruleset_hash and guardrail_verdict. Yes (standalone entry-point scan of incoming external content)
agent_handoff One agent passed work to another. Records input_hash (target ID) and output_hash (handoff payload). No
sanitisation Explicit removal of named taint tags. The ONLY operation permitted to shrink the taint set. The corpus field carries "removed:<tag1>,<tag2>,…". No
merge N parents combine into one. Records output_hash. MUST have ≥ 2 parents. No

Implementations MAY define vendor-specific operation types using a x-vendor-name:operation prefix. Verifiers that do not understand such an operation MUST reject the receipt (rather than ignoring it) to preserve safety properties.

A receipt’s own hash — the value that descendants cite in their parents field — is computed as:

receipt_hash = "sha256:" + lowercase_hex(sha256(ASCII(compact_jws_string)))

i.e. the SHA-256 of the full compact-JWS wire form, prefixed with sha256:.

Hashes of arbitrary text and objects use the same prefix:

hash_text(s) = "sha256:" + lowercase_hex(sha256(UTF-8(s)))
hash_object(o) = "sha256:" + lowercase_hex(sha256(canonical_json(o)))

The sha256: prefix is mandatory and reserves namespace for future hash algorithm migrations.

taint is a sorted set of string tags marking the provenance of untrusted, external, or otherwise non-pristine data.

Tag prefix Meaning
external_user Originates from a user-controlled input boundary.
rag:<corpus> Retrieved from the named corpus. Auto-applied by retrieval operations unless the corpus is explicitly attested as trusted.
guardrail-scan:<target> Touched by a guardrail scan (input / output / tool_call).
x-vendor-name:<tag> Vendor-specific tags. SHOULD NOT collide with reserved prefixes.

Implementations MAY define additional unreserved tags. Verifiers MUST treat unknown tags as opaque strings.

For any non-sanitisation receipt R with parents P₁, P₂, …, Pₙ:

taint(R) ⊇ taint(P₁) ∪ taint(P₂) ∪ … ∪ taint(Pₙ)

A sanitisation receipt MAY shrink the inherited set, but ONLY by tags explicitly listed in its corpus field after the removed: prefix:

taint(sanitisation_R) = (taint(P₁) ∪ … ∪ taint(Pₙ)) \ removed_tags

where removed_tags is parsed from corpus.split(":")[1].split(",").

Verifiers MUST enforce this invariant. Violations indicate either tampering or a buggy emitter and MUST be reported.

Sanitisation authority (deny-by-default). Clearing taint is the one operation that weakens a security property, so it is gated on authorisation, not merely on being well-formed. When a verifier is supplied capability statements, every tag in removed_tags MUST be permitted by the emitting key’s sanitisation_authority (see §5.1): a key with an absent or empty sanitisation_authority MUST NOT be able to clear ANY tag. A sanitisation receipt that removes a tag the key is not authorised to clear MUST be reported as invalid. Without this check, any holder of any enrolled key could launder untrusted data by self-issuing a sanitisation receipt — defeating the taint property the graph exists to provide.

A chain is an append-only sequence of receipts persisted together. The canonical on-disk format is JSON Lines (one receipt per line). Each line is a minimal envelope carrying exactly two fields:

{"receipt_hash": "sha256:...", "jws": "<header>.<payload>.<sig>"}
{"receipt_hash": "sha256:...", "jws": "<header>.<payload>.<sig>"}
Field Description
receipt_hash The content-addressed hash of the JWS (see §7). MUST equal sha256(jws); mismatch indicates tampering.
jws The full compact JWS wire form — the single authoritative signed artifact carrying the entire payload.

The envelope MUST contain ONLY these two fields. A verifier MUST reject any line carrying an additional top-level field (and MUST reject duplicate keys), UNLESS that field is an explicitly registered versioned extension; v1 registers none. There is no wildcard/prefix allowance — an unrecognised field is unsigned, unvalidated surface and is rejected, not ignored.

Verifiers MUST recompute the receipt hash from the jws and compare it to the stored receipt_hash; all payload data MUST be read from the jws, never from the envelope.

Compatibility note (v0.17). Earlier implementations denormalised the full payload into each envelope line. That format is no longer accepted (the denormalised copies were unsigned and unvalidated). Convert a legacy chain with raucle provenance migrate-envelope <chain> --out <new> --pubkeys <keys...>, which strictly parses and signature-verifies each embedded JWS against the supplied keys before rewriting the minimal envelope (a receipt that fails verification is not migrated).

Implementations MAY persist chains in other formats (SQLite, Postgres, S3 object per receipt) provided the canonical JSONL form can be reconstructed without loss.

To verify a chain, an implementation MUST:

  1. Parse each line. On JSON or JWS parse error, mark the chain invalid and report the line.
  2. For each receipt: recompute sha256(jws) and compare to the stored receipt_hash. Mismatch → tampered.
  3. Look up the public key for agent_key_id from the verifier’s configured key store (typically a directory of capability statements). Missing key → unverifiable.
  4. Verify the JWS signature over the <header>.<payload> segment using the public key.
  5. After all receipts are loaded, walk every receipt’s parents list and confirm each parent exists in the chain. Missing parent → broken DAG.
  6. For every non-sanitisation receipt, verify the taint monotonicity invariant (§8.2). For every sanitisation receipt, verify that taint(R) == (union of parent taints) \ removed_tags.
  7. When the verifier is supplied capability statements, it MUST additionally enforce, for each receipt whose agent_key_id is covered: a. the receipt’s agent_id matches the statement’s agent_id; b. iat is at or after issued_at and (if present) strictly before expires_at; c. every model_call / tool_call invokes a model / tool the statement permits; d. every sanitisation receipt only clears tags within the key’s sanitisation_authority (§8.2). An agent_key_id that is not covered by any supplied statement MUST be treated as a violation (fail closed), not silently trusted. A verifier configured with no capability statements MAY skip step 7 (signature + DAG + taint monotonicity still apply).

The verifier’s overall verdict is valid = True if and only if all of the above pass for every receipt in the chain.

The format defends against:

  • Tampering with past receipts (caught by step 2 above).
  • Forging receipts without the emitting agent’s private key (caught by step 4).
  • Silently dropping taint to launder untrusted data (caught by step 6 — and, for sanitisation, by the deny-by-default sanitisation_authority check in step 7d: an enrolled key cannot clear tags it is not authorised to clear).
  • Invoking models or tools the agent is not permitted to use (caught by step 7c).
  • Impersonating another agent’s identity by signing a receipt whose agent_id differs from the key’s capability statement (caught by step 7a).
  • Using an expired authority (caught by step 7b).

The format does NOT defend against:

  • A compromised agent private key. An attacker with the key can emit receipts indistinguishable from the legitimate agent. Mitigation: short-lived keys, capability-statement expires_at, future revocation extension.
  • A lying agent. An agent can record input_hash of one prompt while passing a different prompt to the model. Mitigation: pair with TEE-attested inference (out of scope for v1).
  • Replay. A valid receipt may be re-emitted into a different chain. Mitigation: iat and content-hashes make replay detectable, but the spec does not mandate replay-window enforcement.

Implementations MUST include "raucle/v1" in the JWS crit array. This prevents JWT-conformant libraries from silently accepting provenance receipts as bearer authentication tokens (RFC 7515 §4.1.11 requires conforming verifiers to fail on unrecognised crit entries).

Implementations MUST NOT place raw prompt or output content in the normative v1 payload. Privacy of source content is the most fragile property of the format and MUST be preserved by default. Vendor extensions that opt-in to content storage SHOULD be explicit in their naming (e.g. x-acme-content-cleartext).

The receipt typ field (provenance-receipt/v1) is the version anchor. Future incompatible changes MUST increment the version (v2, v3, …) and SHOULD provide a documented migration path.

Verifiers SHOULD support multiple typ versions concurrently. A receipt’s version is established by its typ header and typ payload (which MUST agree).

Reference test vectors for v1 are published in the spec repository (the authoritative, always-current source; pin a release tag for a stable snapshot):

https://github.com/epic28-ltd/raucle/blob/main/docs/spec/provenance/v1/test-vectors.json
raw: https://raw.githubusercontent.com/epic28-ltd/raucle/main/docs/spec/provenance/v1/test-vectors.json

The vector file has three arrays, and an implementation claiming v1 conformance MUST satisfy all three:

  1. vectors — full receipts, each effectively (seed, expected_jws, expected_receipt_hash). From the shared fixed_seed_hex, an implementation MUST reproduce every expected_jws and expected_receipt_hash byte-for-byte.
  2. canonicalization_vectors — pure §4.3 canonicalisation targets, each (input_object, expected_canonical_utf8, expected_canonical_hex, expected_canonical_sha256). An implementation MUST produce, for each input_object, canonical bytes equal to expected_canonical_hex (and hence the same SHA-256). These isolate the canonicalisation step so a peer can byte-diff §4.3 directly (e.g. non-ASCII strings, key ordering, the safe-integer boundary) without reconstructing a receipt.
  3. invalid_canonicalization_vectors — inputs that are invalid signed/hashed material and MUST be rejected (not serialised), each (input_object, must_reject: true, reason). These make the normative §4.3.5–§4.3.6 rejection rules (floats; integers outside ±(2⁵³−1)) machine-checkable rather than prose-only.

Five interoperable reference implementations, cross-verified to byte-identical content-addressed IDs, are MIT-licensed (deliberately permissive — embed them in any product, including proprietary ones):

  • Python: raucle — raucle/provenance.py (the canonical implementation).
  • TypeScript / Go / Rust / C#: raucle/reference/.

A Vercel AI SDK middleware that gates tool calls and emits these receipts lives at reference/vercel-ai-middleware.

Version Date Notes
v1 Draft 2.2 (pq1) 2026-09-05 Companion profile added: raucle/pq1 hybrid post-quantum signatures — receipts signed by both Ed25519 and ML-DSA-65 (FIPS 204), valid only if both verify, downgrade-stripping rejected by design. Additive: all v1 receipts verify unchanged. Reference vectors: v1/pq1-test-vectors.json.
v1 Draft 2.1 2026-09-01 Companion design document added: conformance re-verification layer (field-level Merkle commitments with selective disclosure over committed call arguments; partial constraint re-check; forward-compatible with a future x_args_commitment receipt extension field). No normative wire-format changes.
v1 Draft 1 2026-05-14 First publication. Subject to non-breaking clarifications during the comment period; breaking changes will be issued as v2.
v1 Draft 2 2026-06-01 Clarifications only (no wire-format change). Documented the deny-by-default sanitisation_authority capability field (§5.1, §8.2, §10, §11) that gates taint clearing; made the verifier identity / expiry / sanitisation-authority checks normative (MUST when capability statements are supplied; §10 step 7); tightened the agent_id regex to forbid a trailing dot and .. (§5), matching dot-delimited scope semantics. Aligns the spec with the reference implementation; existing signed receipts remain valid.

This format borrows ideas from:

Section titled “A. Related work — how this differs (non-normative)”

This format is capability discipline plus a content-addressed, offline-verifiable artifact plus a machine-checked policy binding. The pieces are not new; the combination is the point. How it relates to adjacent work:

Prior art What it gives you What the Provenance Receipt adds
Macaroons / Biscuit / ZCAP / UCAN (capabilities + attenuation) Unforgeable, attenuable bearer tokens A token is permission; the receipt is evidence of what happened, content-addressed and chainable into an audit DAG. The cited policy is additionally SMT-proven sound over the tool’s schema, not just asserted.
JWT / JWS Signed claims We are JWS — this is a profile of it (typ, crit, EdDSA) with canonical-JSON content addressing, taint monotonicity, and an operation/parents model on top.
W3C Verifiable Credentials / OID4VP Who the holder is and is qualified to do Identity, not action-evidence. Complementary: a VC says “this agent is authorised”; the receipt records “this action ran, against this proven policy” — they chain by content hash.
in-toto / SLSA Attestations over the software supply chain (build, release) Same philosophy, different layer: this attests agent runtime actions (tool calls, retrievals, hand-offs) with taint. A receipt could reasonably be expressed as an in-toto predicate type; portability is the goal, not a silo.
C2PA Provenance for media assets Same “sign the artifact’s history” idea, scoped to content; this scopes it to agent actions and authorisation.
Sigstore / Certificate Transparency Keyless signing; append-only public logs Borrowed: the receipt is offline-verifiable against a published key, and chains form a tamper-evident log. We do not (yet) mandate a public transparency log; deployers publish their issuer key.

The deltas that are genuinely ours: (a) the policy a capability cites is machine-checked (Z3) sound over the tool’s JSON Schema, with gate/attenuation soundness mechanised in Lean 4; (b) the receipt is a portable, content-addressed record of the action, not just the grant; (c) monotone taint across the chain. We do not claim to be first on capability tokens, attenuation, signed receipts, or offline verification — several systems ship those.

B. Canonical form — design rationale (non-normative)

Section titled “B. Canonical form — design rationale (non-normative)”

The receipt signs JCS-canonicalised JSON (RFC 8785, integer-only subset), then content-addresses the resulting JWS bytes. Canonical JSON is known to be a footgun — independent implementations drift. The mitigation is structural: five reference implementations (Python, TS, Go, Rust, C#) are pinned to a shared test vector and cross-verified to produce byte-identical receipt IDs, so canonicalisation drift is caught mechanically rather than trusted. JSON/JOSE was chosen for inspectability and ecosystem fit; a COSE/CBOR profile is a reasonable future binding and welcome — the payload model is encoding-agnostic.

C. Token lifetime and revocation (non-normative)

Section titled “C. Token lifetime and revocation (non-normative)”

Capability tokens are short-lived bearer credentials; the model is layered, not a CRL:

  1. Short TTLs — capabilities are minted per-session/per-task with short expiries (examples use 60–300s), so the default revocation latency is the TTL.
  2. Attenuation — long-lived authority is narrowed to per-action child tokens, limiting blast radius.
  3. Gate denylist — a gate accepts a revoked_token_ids set (and a revoke() method); a revoked token, or any child citing it as parent_id, is DENY’d before expiry.
  4. Key rotation — removing an issuer key from a gate revokes everything it signed.

There is intentionally no per-token CRL in v1; a signed, distributable revocation feed for multi-gate fleets is scoped as a future addition. This is a deliberate design choice for a bearer-token system, documented rather than implied.

Issues and proposed changes: