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. For the design-level view of the same object — envelope, verification semantics, replay rules and trust boundaries, without the field-by-field contract — see the public Decision Receipt specification. Where the two disagree, this page is correct and that one is wrong.

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. The same discipline governs the three shadow-mode fields, which joined the hash payload after both schema versions had shipped: a receipt is hashed over the keys it actually carries, so receipts minted before a field existed keep verifying unchanged (§1.6, §2.2).

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? }. The whole action object is bound into integrity_hashmetadata included. The payload binds action as a single value, not a selection of its keys (§2.2), so changing any part of it changes the hash. This is unlike context.source_artifact (§1.3), where only hash is bound.

1.6 Shadow-mode fields (result.is_shadow, result.shadow_converted, result.original_decision)

Three optional fields on result record how shadow-mode policies took part in the decision.
Conversion rule. A DENY with at least one violation, where every violation carries is_shadow: true, is recorded as ALERT; shadow_converted: true and original_decision: 'DENY' are written alongside it. DENY is the only convertible verdict — an all-shadow REVIEW stays REVIEW. The conversion happens inside the evaluator, before the hash is computed, so the suppressed verdict is part of the signed surface rather than a note attached afterwards. Participation is not suppression. is_shadow says a shadow-mode rule was applicable; shadow_converted says a shadow-mode rule changed the verdict. A receipt can carry is_shadow on its own — a shadow rule ran, but the outcome was not an all-shadow DENY. violations[i].is_shadow (§1.1) is a third, separate field: it marks one violation as coming from a shadow-mode policy, and it is bound because the whole violations array is bound. Where the stamp comes from. Shadow status reaches the evaluator on the rules of the frozen policy snapshot, not from live policy state. Replaying a receipt against its own snapshot therefore re-derives the same conversion, and a snapshot whose shadow stamps were altered no longer reproduces the receipt. Binding. All three are bound into integrity_hash when present, on the v1 and v2 paths alike (§2.2). None of them is named in the v2 signing envelope; the signature covers them transitively, because the envelope binds integrity_hash (§2.3).

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:
Shadow-mode fields — presence-dispatched, identical on both paths. Up to three further keys join the payload above, and the v1 and v2 builders dispatch them the same way (§1.6):
Dispatch is on presence, not value: a key is included whenever its field is not undefined, and the value is then bound verbatim. shadow_converted and original_decision are a pair — the payload carries both keys or neither, and original_decision is coerced to null if the pair is written without it. is_shadow dispatches independently of that pair, because the two joined the payload at different times; a receipt may bind one, the other, both or neither. Where a field is absent, its key is absent. Receipts minted before a field joined the payload — and every receipt where MeshQu had nothing to write — therefore hash over the payloads exactly as listed above, unchanged, and go on verifying. Stripping a field from a receipt that carries it, or adding one to a receipt that does not, changes the recomputed hash and fails verification. There is no cross-epoch fallback. Key order stays alphabetic however these keys are added (§2.1). Not in the integrity hash: transparency_anchor (computed after signing), chain fields, evaluation_time_ms, rules_evaluated / rules_na / na_rules, the actor row, 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. Hash coverage is not envelope coverage. The v2 envelope has exactly the seven fields listed above. Every other receipt field the signature covers — including the shadow-mode fields of §1.6 — is covered transitively, through integrity_hash: the signature attests to the hash, and the hash is computed over those fields. v1, which signs the bare integrity_hash string, covers them the same way. Being inside the integrity hash and being named in the signing envelope are separate properties; §14 lists which fields have which. 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. Shape-checked only. The verifier emits transparency.inclusion_proof_invalid when the bundled proof’s inclusion_proof is present and malformed. The cryptographic binding comes from the SET, so the verifier also emits the advisory transparency.inclusion_proof_unverified whenever it cannot complete SET verification — for a legacy anchor lacking the new fields, and whenever the caller supplied no Rekor trust roots, which is the default. That code is therefore not a legacy-only signal.
Every bullet above describes a bundle that actually ships transparency_proof.json. What the verifier reports when it does not is §6.1. Online Rekor access splits into three layers. Read them separately — the differences are where public copy usually goes wrong:
  • Online entry-existence lookup: Implemented. verifyRekorInclusion() queries the log by entry UUID and reports whether an entry exists there. It is exported from @meshqu/core and tested. The shipped browser verifier also makes live, host-allowlisted calls to Rekor when validating an anchor. This confirms an entry is present; it does not verify an inclusion proof.
  • Bundle-verifier wiring: Inert. BundleVerificationOptions.online exists but is documented as “reserved for future wiring”. Bundle verification is offline by default and setting the flag changes nothing today.
  • Co-signed-witness inclusion-proof verification: Planned. Verifying an inclusion proof against an independently co-signed log witness is not implemented. Offline SET verification against a pinned Rekor log key remains the guarantee MeshQu stands behind.

6.1 What a bundle reports for an anchor

The anchor is recorded on the receipt, not proved by it. It is written after signing and is deliberately not covered by the integrity hash (§14). What the bundle verifier reports therefore depends on what the bundle carries, and there are four distinct outcomes: The branches are tested in this order, so the more specific case wins: Verdicts and exit codes are §10.5. A pre-PR-5b/5f anchor (no dsse_envelope / entry_body) falls in B: the exporter deliberately ships no proof for it, so the verifier has nothing to check the claim against. B changed on 2026-08-28 (CCR-302c). It previously reported not_applicable with no failure code, verdict valid, exit 0. not_applicable asserts that no check was required; not_checked states that one was not performed. When the verifier lacks the inputs to determine anything, the second is true and the first is a claim that cannot be supported. Note the shape of D in the default configuration: with no Rekor trust roots supplied, a correctly anchored bundle reports transparency: invalid carrying only the advisory transparency.inclusion_proof_unverified, which rolls up to warn and exit 0. A sub-claim reading invalid is not by itself a failed bundle — the failure codes decide (§10.5). Two consequences to state plainly on any public surface:
  • An anchor displayed on a receipt is a claim recorded, not a claim verified. provider, log_index, entry_uuid and anchored_at render as facts on every surface that displays a receipt. Only A and D involve the verifier checking anything about them.
  • The verifier cannot distinguish “never anchored” from “anchor removed”. Because the anchor is not bound by the integrity hash, a receipt whose anchor was stripped to null presents the same claim as one that was never anchored, and transparency_anchor: null is a legitimate state that unanchored runs export on purpose. Do not read C as evidence that no anchoring took place.

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 a status and a failure_codes: string[]. There are four statuses: not_applicable and not_checked are not interchangeable, and the distinction is load-bearing rather than stylistic: the first asserts that no check was required, the second that one was not performed. Only the second carries verification debt. Shadow-mode fields and the two hash-comparing sub-claims. integrity and snapshot_replay both recompute a hash, so both have to account for the presence dispatch of §2.2. A fresh replay re-derives shadow conversion from the shadow stamps on the bundled snapshot’s rules, and the replay result is then normalised to the fields the stored receipt actually carries before the hashes are compared. The normalisation removes the shadow keys only — never the verdict — so a stored decision that disagrees with what the bundled snapshot produces still fails snapshot_replay.hash_mismatch, and a receipt whose shadow fields were stripped or added after signing fails integrity.mismatch.

10.4 Failure codes

These are the codes this page describes. It is not the complete set. The gap is stated here rather than left to be discovered:
  • @meshqu/types declares 55 BundleFailureCode members. 40 are listed below. The other 15 are emitted by the shipped verifier and are not yet described on any public surface; they belong to legs that have not reached step 4 of §18.
  • @meshqu/verify-lib — the browser verifier, a deliberately independent re-implementation — declares its own union carrying 17 further codes. One of them, snapshot_replay.skipped_in_browser, is an advisory recording that the browser has no evaluator and skipped that sub-claim there. The other 16 belong to a separate verifier leg (CCR-403) whose capability §15 does not list; until §15 lists it, no public surface may claim it.
  • Across both packages the union is 72 codes, of which 32 are not listed below.
These five numbers are no longer prose. docs-parity.invariant.test.ts in the tradequ repository parses them out of this section and compares them with the declared unions, so a code added to either package and not reflected here turns that check red. Its limits are stated in the test itself. A code’s absence from this list is a documentation gap, not evidence that the verifier cannot emit it. Do not treat the list as an exhaustive contract, and do not build a public claim on a code being missing from it.

10.5 Verdict roll-up and exit codes

The ten sub-claims roll up to one headline verdict, with precedence invalid > indeterminate > warn > valid: An advisory code is one the verifier emits to disclose a scope limit rather than to report a cryptographic failure. transparency.inclusion_proof_unverified and snapshot_replay.skipped_in_browser are the two this page names; the set is not fully documented here (§10.4). A sub-claim is downgraded only when every code on it is advisory — a real failure alongside an advisory one always wins, so “we could not check X” can never launder “X failed”. 5 and 7 answer deliberately different questions. Exit 5 says a check ran and the claim is refuted. Exit 7 says a claim was made and could not be established: the artefact is not accused of anything, and it has not passed either. Automation gating on verification should pick the question it means:
  • -eq 0nothing is wrong. Rejects unchecked claims.
  • -ne 5nothing is proven wrong. Accepts unchecked claims.
Neither is the right default for every caller, which is why the verifier reports them separately rather than collapsing 7 into either neighbour. A CI gate written as “exit 0 or fail” is a stricter policy than one written as “not exit 5”, and the difference is exactly the set of claims the artefact makes that the verifier could not settle. The verdict is the shipped verifier’s own output, in both the Node and browser implementations. The exit codes are the mapping the command-line verifier applies to it; that verifier is not published yet — see Trust Model for its status.

11. Public API Surface

The endpoints exposed by the MeshQu API: Fourteen path patterns are reachable with no API key and no tenant header — eighteen when the operator enables the OpenAPI document and docs UI. Nine of them reach tenant data: the eight reads in the table above plus one write, the form submit. The remaining five are infrastructure, with one caveat worth stating — /metrics exposes per-tenant time series and is guarded by a bearer token rather than by the API key. All of them are listed, with what “unauthenticated” does and does not mean, under Authentication → Public paths. 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.
Tenant isolation is enforced in four layers — the request’s tenant header, an application-level tenant predicate on every query, a per-transaction SET LOCAL of the tenant id, and Postgres row-level security with FORCE — so a request carrying a tenant header can only ever reach that tenant’s data. What that does not say. Of the fourteen path patterns reachable with no tenant header and no API key (§11), nine reach tenant data — and they read and write it, addressed by an unguessable identifier rather than by an authenticated principal. A receipt id, chain id or form id is therefore a capability: whoever holds one can read what it names, and in the case of POST /v1/forms/:formId/submit cause a signed receipt to be minted under that form’s tenant. On those paths the four layers are not what separates one tenant from another, because they are not in the request at all — GET /v1/receipts/:decisionId resolves through a SECURITY DEFINER lookup that takes a receipt id and no tenant predicate. What keeps one tenant’s receipts out of another’s reach there is the unguessability of the identifier, and nothing else. Publish an id and you have published what it names.

13. Key Management

  • Signing key kid format: msk_v{N} (e.g. msk_v1).
  • Algorithm: Ed25519 only.
  • trusted_keys.json inside a bundle is informational only and is never used as a trust root. Treating it as one would let a self-signed bundle — operator-controlled keys listed in the bundle as “trusted” — verify against itself. The verifier refuses instead: with no external roots supplied it emits signature.no_external_trust_root (§10.4) rather than falling back to the bundled listing.
  • Production verification requires trust roots supplied out-of-band. The verifier resolves a receipt’s signature_kid only against roots its caller passes in; with none supplied it emits signature.no_external_trust_root rather than falling back to anything. How a verifying party obtains and pins those roots is that party’s own policy — it is not something the issuer’s own API can settle, and this page does not tell you to fetch them from one. GET /v1/.well-known/signing-keys in particular is deprecated and self-asserted — the producer vouching for itself — and must not be treated as a trust root.
  • The /verify demo is not an exception to that rule so much as a demonstration of it: it verifies against a bundled demo key (msk_demo_v2) and makes no trust-rooted claim.
  • Key rotation does not retroactively invalidate bundles — a rotated-out key still verifies a receipt it signed, provided the verifier’s out-of-band root set still carries it. The bundle’s trusted_keys.json records which keys were current at export time, which is useful for a fingerprint UI; it is not what makes the old signature verify.
  • 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, which is stripped whole), source_artifact.hash, action in full — metadata included (§1.5) — policy_snapshot_id, evaluated_rules_hash, timestamp. Additionally bound into v2 integrity_hash: receipt_schema_version, policy_snapshot_digest, evidence_manifest_digest (or null). Conditionally bound into integrity_hash — v1 and v2 alike — when the field is present on result: is_shadow, shadow_converted, original_decision (§1.6). Presence-dispatched: absent field, absent key, unchanged legacy payload (§2.2). In the v2 signed envelope (§2.3) — seven fields exactly, in two groups:
  • Envelope only, not in the integrity hash: signature_kid, signature_algorithm.
  • In the envelope and in the integrity hash: timestamp (bound by the v1 and v2 hash alike), receipt_schema_version, policy_snapshot_digest, evidence_manifest_digest.
The seventh field is integrity_hash itself, which is what makes the signature’s coverage of every other field transitive (§2.3). Stored on the receipt but NOT hashed: transparency_anchor, chain linkage, evaluation_time_ms, rules_evaluated, rules_na, na_rules, source artifact metadata (type / filename / byte_size / reference_id), 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.

18. Publication order for wire-format changes

When the receipt wire format changes, the surfaces that describe it must be updated in a fixed order. Each step gates the next — nothing at a given step may describe a change until every step above it is complete. This page is step 4, which cuts both ways:
  • Nothing downstream of it may run ahead. Developer guides, website copy and visuals, decks, event pages and sales collateral must not describe a wire-format change before this page describes it.
  • This page may not run ahead either. It may not describe a wire-format change before the signed public fixture (step 3) exists. Receipt-shaped examples are generated from, or explicitly checked against, a valid fixture; hand-authored ones are not permitted on any public surface.
Standing embargo — receipt v3. No public surface may describe receipt v3 fields, semantics or timelines until steps 1–4 are complete. That includes this page. Until then, v3 is not a public roadmap item and no date may be given for it. Today’s wire format is receipt_schema_version v1 and v2 only (§1.1). The same order is recorded as a standing rule alongside the implementation, so that engineering and documentation cannot drift apart; this section is its docs-side statement, and this page remains the authority for what public surfaces may claim.

19. Agent Licences

A first-class signed record answering: which named person authorised this API credential to act, under which ratified policy version, for which decision types, until when — and has that authorisation been withdrawn? Everything in this section is validated against the committed public fixture — a real grant issued 2026-08-25 in a purpose-made demo tenant, captured from the public lookup and verified with the open verifier: packages/meshqu-core/test-vectors/public-agent-licence-fixture/ in the platform repository. The field contract is docs/architecture/0004-agent-licence-digest-contract.md (ADR 0004).

19.1 The record

Two record types share one shape and one table: a grant, and a revocation that supersedes it. Revocation is a new signed record naming the grant — never an edit or a deletion. The store is append-only. The signed preimage is exactly 14 fields (ADR 0004 §3): id, tenant_id, record_type, key_id (the subject — an API key, not a person and not a model), revokes_licence_id, granted_by (the authenticated user id captured at issue), grant_authority (free text, stored verbatim), policy_id, policy_version_id, policy_version_number, scope (decision types, matched exactly), valid_from, valid_until, issued_at. licence_digest = SHA-256 over the canonical JSON of that preimage. grantor_signature is Ed25519 over the digest, under a ratifier key — policy-governance authority, disjoint from receipt-signing keys by construction. Verification recomputes the digest before checking the signature: a genuine signature over a stale digest proves nothing about an edited record.

19.2 Lifecycle and admissibility

Issue and revoke are human governance acts: the routes reject any API-key principal before the handler runs. A deployment resolves licence admissibility on every receipt-minting call under one of three modes — off, record_only, enforce — where the effective mode is the maximum of the deployment default and the tenant setting. In record_only, the admissibility status is stamped into the decision receipt’s hashed context and nothing is refused; in enforce, an expired, out-of-scope, revoked or missing licence fails closed with a distinct error code and no receipt is minted. The mode itself is stamped into the hashed context, so a record-only receipt is distinguishable from an enforced one from the receipt alone.

19.3 Verification and the public lookup

GET /v1/receipts/:id (public, unauthenticated) resolves a licence id and returns the grant with its superseding revocation inline (or null), so a verifier can distinguish verifiably issued from verifiably still in force. The open verifier (@meshqu/verify-lib/agent-licence) recomputes the digest from the response bytes, checks the signature against the ratifier trust roots, verifies any revocation the same way including its binding to the grant, and reports cryptographic verdict and lifecycle as two separate axes — a revoked licence is normally cryptographically perfect, and one badge would have to lie about one of the two.

19.4 What a licence does and does not establish

  • It evidences governed delegation by a named accountable person. It does not discharge, transfer or dilute that person’s accountability, and it is not a regulator-granted permission.
  • Its subject is an API credential. It does not establish which model or agent implementation acted behind that credential.
  • The signature is checked against the trust roots the verifier carries. The stronger claim — verification without trusting MeshQu — awaits the published signed key registry and may not be made until then.