Skip to main content
This is the authoritative reference for the Decision Receipt and every feature attached to it. It is intended as a working spec when writing or reviewing public surfaces (homepage, /spec, /verify, sales/regulatory copy) so that what is presented externally aligns with what the technology actually does. If a property is not listed here as Implemented, do not claim it. Statuses use: Implemented, Partial (real but limited), Stub (wired but currently no-op), Planned. This page describes the receipt as it appears on the wire — the shapes the API returns and what each field binds. It is a working spec for public surfaces, not an implementation guide.

1. The Receipt Envelope

A Decision Receipt on the wire is exposed as { context, result, chain_id, chain_step }. The cryptographic centre of gravity is result (the EvaluationResult).

1.1 v1 vs v2 — which fields exist

Top-level (alongside the receipt, not inside result): id (UUID), tenant_id, actor (DecisionActor JSON), chain_id, chain_step, parent_decision_id, created_at (recorded-at, distinct from result.timestamp). Migration semantics. v1 receipts continue to verify forever. There is no cut-over date. Cross-version fallback is rejected — a v2 receipt must verify under the v2 path; a v1 receipt must verify under the v1 path.

1.2 Decision Context (the input)

Type: DecisionContext.
context.evidence[] is the legacy reference-only evidence list. The richer Evidence Manifest (§5) is a separate first-class concept and its digest is stored on result, not context.

1.3 Source Artifact (context.source_artifact)

Type: SourceArtifact. Reference-based: stores { type, hash, hash_algorithm: 'SHA-256', reference_id?, filename?, byte_size? }. Only hash is bound into integrity_hash. Type, filename and size are convenience metadata and intentionally outside the hash. This is the older single-artifact binding (“Trust Without Disclosure”). It co-exists with the Evidence Manifest in v2; deprecation is out of scope.

1.4 Actor (the actor field)

Type: DecisionActor.
MeshQu does not validate actor identity — the caller attests. metadata.actor_id on context is bound into the integrity hash via context_hash; the structured actor row is stored alongside but not part of the integrity hash.

1.5 Action (result.action)

Type: ReceiptAction. Shape: { type, reference_id, metadata? }. type and reference_id are bound into integrity_hash. metadata is not.

2. Cryptographic Model

2.1 Canonicalisation profile

meshqu-canonical/v0 — frozen. Alphabetic key order, JS Array.sort. RFC 8785 alignment is a planned P1 follow-up that will introduce meshqu-canonical/v1.

2.2 Integrity hash — exact payloads

v1 inputsha256(canonicalJson(payload)) where payload is, alphabetised:
v2 input — extends v1 with:
Not in the integrity hash: transparency_anchor (computed after signing), chain fields, evaluation_time_ms, rules_evaluated / rules_na, the actor row, action.metadata, source artifact metadata.

2.3 Signing — what is signed

  • v1 signs the bare 64-char hex integrity_hash string, UTF-8 encoded.
  • v2 signs the canonical envelope, alphabetised:
The v2 envelope binds signature_kid, signature_algorithm and receipt_schema_version into the signed bytes — closing the gap where v1 stored these alongside but not inside the signature. The verifier reconstructs envelope bytes using the receipt’s stored signature_kid / signature_algorithm (not hardcoded) so any tampering of those fields fails the signature check. Algorithm: Ed25519 only in v1+v2. Other algorithms require a manifest-version bump.

2.4 Policy snapshot digest (v2)

computePolicySnapshotDigest(snapshot) = sha256(canonicalJson({ id, rules, policy_versions, created_at })). This binds the content of the policy, not just its UUID. Approval-receipt digests live on policy_versions[i].approval_receipt_digest, so approval lineage is bound transitively through this single digest — there is no separate top-level approval digest on the receipt.

2.5 Evidence manifest digest (v2, optional)

sha256(canonicalJson(manifest minus manifest_digest)). Self-describing pattern (mirrors the bundle manifest). MeshQu computes and verifies this digest as part of v2 receipt and bundle handling.

2.6 Active-rule projection (used everywhere rules are hashed)

projectActiveRules strips inactive rules and all metadata, keeping only { code, condition, severity, when? }. This single helper is used by:
  • computeSnapshotHash (snapshot repo + evaluator)
  • PolicyApprovalReceipt.policy_rules_hash
  • PolicySnapshot.policy_versions[i].policy_rules_hash
  • the bundle verifier when comparing per-version hashes
Changing the projection is a hash-migration event.

3. Policy Snapshot

A frozen, immutable copy of the rules that governed a decision. Identifies the policy run by both UUID and content digest in v2.
  • id (UUID, on the receipt as policy_snapshot_id)
  • rules[] — full PolicyRule[]. Bundled in policy_snapshot.json; not stored on the receipt itself.
  • policy_versions[] — list of { policy_id, version, policy_rules_hash, approval_receipt_digest? }
  • created_at (ISO 8601)
  • evaluated_rules_hash on the receipt = sha256(canonicalJson(projectActiveRules(rules)))
  • policy_snapshot_digest on the receipt (v2) = sha256(canonicalJson({ id, rules, policy_versions, created_at }))
Replay = re-run the snapshot’s rules against the original context and check the verdict matches. The bundle verifier sub-claim is snapshot_replay.

4. Approval Receipts (lineage)

Status: Implemented end-to-end (APR-008 / APR-009 / APR-010, May 2026). Issuance, storage, bundle export, Node verifier and browser verifier all verify approval lineage. Pinned by golden fixtures, an 18-row mutation matrix and Node ↔ browser parity tests.
  • Policy approval receipts are a first-class artifact. Each receipt holds a ratifier_signature (Ed25519, kid + algorithm), policy_rules_hash (via projectActiveRules) and a self-describing approval_receipt_digest.
  • A bundle ships these in policy_approval_receipts.json.
  • The verifier sub-claim approval_lineage activates when the receipt is v2 and the bundled snapshot has at least one non-null policy_versions[i].approval_receipt_digest. For every such version the verifier checks:
    1. The receipt is present in the bundled file (keyed by policy_version_id).
    2. computeApprovalReceiptDigest(receipt) equals the snapshot entry’s approval_receipt_digest.
    3. receipt.policy_rules_hash equals entry.policy_rules_hash by string equality (do not re-hash snapshot.rules — APR-006 precomputed per-version hashes that cover only that version’s rules, and the snapshot entry is itself bound by policy_snapshot_digest).
    4. receipt.ratifier_signature.kid resolves in the caller-supplied ratifierTrustedRoots.
    5. The ratifier Ed25519 signature verifies.
  • Reports not_applicable for v1 receipts and v2 receipts whose snapshot has no non-null approval_receipt_digest entries.
  • The same approval-lineage checks run in both the Node verifier and the browser verifier, kept at parity (APR-009).

5. Evidence Manifest (v2 only)

Type: EvidenceManifest. Strictly additive — does not modify v1 receipts or source_artifact.
Each EvidenceItem:
Custodian signatures are produced outside MeshQu (banks, KYC vendors, auditors). MeshQu never holds custodian private keys. Trust roots are supplied to the verifier out-of-band via custodianTrustedRoots. The manifest can carry a mix of signed and unsigned items. The manifest digest covers all items; the per-item signature is an additional, independent attestation by the custodian. Reference-based — the artifact bytes are NOT stored. Only digests, references, and metadata.

6. Transparency Anchor (Rekor)

Type: TransparencyAnchor. Stamped onto the receipt after signing, so it is not part of integrity_hash.
What works today:
  • Anchoring. Implemented. Receipt is submitted to Rekor; anchor data captured.
  • Offline SET verification. Implemented. The verifier reconstructs the canonical message Rekor signed ({body, integratedTime, logID, logIndex}) and checks the SET against a trusted Rekor public key.
  • Offline subject-digest binding. Implemented when dsse_envelope is present. The verifier decodes the DSSE envelope, reads the in-toto Statement and asserts subject.digest.sha256 == receipt.integrity_hash.
  • Inclusion-proof verification. Accepted as transparency.inclusion_proof_unverified for legacy anchors lacking the new fields; emits transparency.inclusion_proof_invalid when present and broken.
What’s not implemented:
  • Online Rekor lookups (live witness fetching): Planned. Only offline SET verification today.
Pre-PR-5b/5f anchors (no dsse_envelope / signed_entry_timestamp / integrated_time / log_id) report transparency: not_applicable.

7. Decision Chains

A chain is an ordered group of receipts that together prove a workflow.

7.1 Linkage on the receipt

ChainLinkage — top-level fields on the receipt:
  • chain_id (UUID, caller-provided)
  • chain_step (1-based, auto-assigned if omitted)
  • parent_decision_id (UUID, optional — explicit causal lineage)

7.2 Chain proofs

The chain-proof artifact (chain_proof.json in a bundle). Each non-first step gets:
  • chain_integrity_hash = sha256 over { chain_id, step, decision_integrity_hash, previous_chain_hash }
  • chain_signature (Ed25519 over the chain integrity hash)
This makes the chain hash-linked: edit any step’s receipt and every later step’s chain_integrity_hash changes.

7.3 Chain seal

The chain-seal artifact (chain_seal.json in a bundle):
  • seal_hash = sha256 over { chain_id, sealed_at, steps_count, ALL receipt_hashes (ordered) }
  • seal_signature (Ed25519 over seal hash)
  • sealed_at (ISO 8601)
The seal is a completeness proof — adding, removing or reordering steps breaks the seal. A sealed chain is a closed proof of the entire workflow.

7.4 Verification result type

ChainVerificationResult exposes chain_proof: 'full' | 'partial' | 'none' | 'failed', per-step status, warnings (gap, parent_not_in_chain, parent_step_not_lower, chain_id_mismatch, proof_boundary) and seal status.

8. Decision Outcomes (post-decision tracking)

Separate from the receipt itself. A decision-outcome record carries:
Use on UI / detail surfaces. Do not market as part of the receipt; it is an audit-grade post-hoc annotation.

9. Audit Trail

Append-only, hash-chained. Each event carries:
  • previous_hash
  • per-event integrity_hash = sha256 over { action, entity_type, entity_id, actor_id, tenant_id, changes, timestamp, previous_hash }
UPDATE / DELETE on audit rows are denied at the row level by DB triggers. A decision_recorded audit event is written on every record.

10. Verification Bundle

A self-describing tar / JSON archive that lets a third party verify a receipt offline. See also the standalone Verification Bundle page; this section focuses on the contract relevant to the receipt.

10.1 Manifest (bundle_manifest.json)

10.2 Files in a bundle

trusted_keys.json is pinned at export time. Rotating signing keys does not retroactively invalidate already-issued bundles.

10.3 Verifier sub-claims

Every relevant sub-claim is reported (not just the first failure). Each has status: 'valid' | 'invalid' | 'not_applicable' and a failure_codes: string[].

10.4 Failure codes (full list)

11. Public API Surface

The endpoints exposed by the MeshQu API: Public receipt response shape: { context, result, chain_id, chain_step }. The API normalises legacy policy_snapshot_hashevaluated_rules_hash on the way out.

12. Persistence Model (summary)

MeshQu persists each of these as a distinct artifact, surfaced through the REST API and verification bundles:
  • Receipts — immutable once written; hash, signature and timestamp fields cannot be updated or deleted after they are recorded.
  • Chain proofs — per-step chain integrity hashes and signatures.
  • Chain seals — a completeness seal per chain.
  • Decision outcomes — post-decision status (override / escalate / etc).
  • Policy snapshots — frozen policy bundles.
  • Policy approval receipts — ratifier-signed approval records.
  • Audit events — an append-only, hash-chained audit log.
Each tenant’s data is strictly isolated; a request can only ever read or write data belonging to its own tenant.

13. Key Management

  • Signing key kid format: msk_v{N} (e.g. msk_v1).
  • Algorithm: Ed25519 only.
  • Key rotation does not retroactively invalidate bundles — each bundle pins its trusted keys at export time in trusted_keys.json.
  • The /verify demo uses bundled msk_demo_v1; production verification uses trusted_keys.json from the bundle.
  • Custodian (evidence) keys and ratifier (approval) keys are supplied to the verifier out-of-band via custodianTrustedRoots / ratifierTrustedRoots. MeshQu never holds them.

14. What Is and Isn’t Bound — quick reference

Bound into v1 integrity_hash: decision, violations, context (minus source_artifact metadata), source_artifact.hash, action.{type, reference_id}, policy_snapshot_id, evaluated_rules_hash, timestamp. Additionally bound into v2 integrity_hash: receipt_schema_version, policy_snapshot_digest, evidence_manifest_digest (or null). Additionally bound into the v2 signed envelope (not the integrity hash): signature_kid, signature_algorithm, timestamp. Stored on the receipt but NOT hashed: transparency_anchor, chain linkage, evaluation_time_ms, rules_evaluated, rules_na, na_rules, action.metadata, source artifact metadata (type / filename / byte_size), the structured actor row, decision outcomes. Bound transitively via policy_snapshot_digest: all policy_versions[i].approval_receipt_digest entries (when present), all policy_versions[i].policy_rules_hash entries — therefore approval lineage.

15. Status Matrix

16. Glossary — safe public terms (and what to avoid)

17. The two highest-risk public-copy traps

  1. Implying evidence content custody. The product is reference-based. Any public surface that says “MeshQu stores the policy, context, evidence and outcome” overshoots. Use evidence references / evidence digests. The single artefact on the receipt that binds content (source_artifact.hash) binds only the hash — type, filename and size are not bound.
  2. Implying live transparency-log verification. Anchoring to Rekor and offline SET verification are real. Online inclusion-proof lookup against a live witness is not implemented yet. Don’t say “checked against the live transparency log” or “live witness verification”. “Anchored to a public transparency log; verifiable offline against a pinned log key” is accurate.