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 |
1. Abstract
Section titled “1. Abstract”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.
2. Goals and non-goals
Section titled “2. Goals and non-goals”2.1 Goals
Section titled “2.1 Goals”- 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.
2.2 Non-goals
Section titled “2.2 Non-goals”- 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.
3. Terminology
Section titled “3. Terminology”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. |
4. Wire format
Section titled “4. Wire format”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.
4.1 JOSE header
Section titled “4.1 JOSE header”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.
4.2 Payload
Section titled “4.2 Payload”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.
4.3 Canonical JSON
Section titled “4.3 Canonical JSON”For both header and payload, prior to base64url encoding:
- 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, Gosort.Strings, Ruststrordering) 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 theparentsandtaintarrays (§4.2) and to the sorted string lists in a capability statement (§5.1). - Separators MUST be
,and:with no whitespace. - The byte encoding MUST be UTF-8.
- Strings containing non-ASCII characters MUST NOT be escaped (i.e.
ensure_ascii=Falsein Pythonjson.dumps, equivalent in other languages). - 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.
- 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.
4.4 Signature
Section titled “4.4 Signature”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).
5. Agent identity
Section titled “5. Agent identity”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]?$. Theagent: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 becauseagent_idscoping is dot-delimited (a token foragent:billingauthorizesagent:billing.invoicebut notagent: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 ofsha256(public_key_pem_pkcs_subject_public_key_info).
5.1 Capability statement
Section titled “5.1 Capability statement”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_modelslist MUST be interpreted as “unrestricted”. The same applies toallowed_toolsanddata_classifications. Implementations that wish to forbid this default MAY require non-empty lists at issuance time. sanitisation_authorityis the set of taint tags this key is permitted to clear via asanitisationoperation (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
issuerfield 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_atMAY be omitted for non-expiring statements. Verifiers that are supplied capability statements MUST reject receipts whoseiatis at or after a statement’sexpires_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.
6. Operations
Section titled “6. Operations”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.
7. Receipt hashes (content addressing)
Section titled “7. Receipt hashes (content addressing)”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.
8. Taint
Section titled “8. Taint”taint is a sorted set of string tags marking the provenance of untrusted, external, or otherwise non-pristine data.
8.1 Reserved tags
Section titled “8.1 Reserved tags”| 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.
8.2 Monotonicity invariant
Section titled “8.2 Monotonicity invariant”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_tagswhere 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.
9. Chains
Section titled “9. Chains”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.
10. Verification algorithm
Section titled “10. Verification algorithm”To verify a chain, an implementation MUST:
- Parse each line. On JSON or JWS parse error, mark the chain invalid and report the line.
- For each receipt: recompute
sha256(jws)and compare to the storedreceipt_hash. Mismatch → tampered. - Look up the public key for
agent_key_idfrom the verifier’s configured key store (typically a directory of capability statements). Missing key → unverifiable. - Verify the JWS signature over the
<header>.<payload>segment using the public key. - After all receipts are loaded, walk every receipt’s
parentslist and confirm each parent exists in the chain. Missing parent → broken DAG. - For every non-
sanitisationreceipt, verify the taint monotonicity invariant (§8.2). For everysanitisationreceipt, verify thattaint(R) == (union of parent taints) \ removed_tags. - When the verifier is supplied capability statements, it MUST additionally enforce, for each receipt whose
agent_key_idis covered: a. the receipt’sagent_idmatches the statement’sagent_id; b.iatis at or afterissued_atand (if present) strictly beforeexpires_at; c. everymodel_call/tool_callinvokes a model / tool the statement permits; d. everysanitisationreceipt only clears tags within the key’ssanitisation_authority(§8.2). Anagent_key_idthat 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.
11. Security considerations
Section titled “11. Security considerations”11.1 Threat model
Section titled “11.1 Threat model”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-defaultsanitisation_authoritycheck 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_iddiffers 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_hashof 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:
iatand content-hashes make replay detectable, but the spec does not mandate replay-window enforcement.
11.2 crit header is mandatory
Section titled “11.2 crit header is mandatory”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).
11.3 No content in the normative payload
Section titled “11.3 No content in the normative payload”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).
12. Versioning
Section titled “12. Versioning”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).
13. Interoperability test vectors
Section titled “13. Interoperability test vectors”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.jsonraw: https://raw.githubusercontent.com/epic28-ltd/raucle/main/docs/spec/provenance/v1/test-vectors.jsonThe vector file has three arrays, and an implementation claiming v1 conformance MUST satisfy all three:
vectors— full receipts, each effectively(seed, expected_jws, expected_receipt_hash). From the sharedfixed_seed_hex, an implementation MUST reproduce everyexpected_jwsandexpected_receipt_hashbyte-for-byte.canonicalization_vectors— pure §4.3 canonicalisation targets, each(input_object, expected_canonical_utf8, expected_canonical_hex, expected_canonical_sha256). An implementation MUST produce, for eachinput_object, canonical bytes equal toexpected_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.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.
14. Reference implementation
Section titled “14. Reference implementation”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.
15. Changelog
Section titled “15. Changelog”| 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. |
16. Acknowledgements
Section titled “16. Acknowledgements”This format borrows ideas from:
- Sigstore — keyless signing for software supply chains.
- SLSA — software supply-chain levels.
- in-toto — attestation format for software supply chains.
- Certificate Transparency — append-only public logs.
- JCS (RFC 8785) — canonical JSON.
A. Related work — how this differs (non-normative)
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:
- Short TTLs — capabilities are minted per-session/per-task with short expiries (examples use 60–300s), so the default revocation latency is the TTL.
- Attenuation — long-lived authority is narrowed to per-action child tokens, limiting blast radius.
- Gate denylist — a gate accepts a
revoked_token_idsset (and arevoke()method); a revoked token, or any child citing it asparent_id, is DENY’d before expiry. - 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.
17. Feedback
Section titled “17. Feedback”Issues and proposed changes:
- GitHub: https://github.com/epic28-ltd/raucle/issues (tag
spec) - Mailing list: TBD (announce when established)