Version 1.0 · published 2026-09-28 · applies to protocol mirror/1 (the spec pin carried on /health and in every record).
Banks sign agreements against what stays frozen. This page says what is frozen within a major version, what can only grow, what is experimental, and how anything is ever removed. Each promise names the test that holds the codebase to it.
Frozen within a major version#
These can change only in a new major version (mirror/2), announced at least 180 days ahead:
- The three verdict states:
reconciled,divergent,undetermined. There is no fourth state, andclearedis true only forreconciled. - The commit primitive. The per-shipment salt derivation, the band commitment preimage (
salt ":" band), the corridor commitment, and exact-decimal banding. Pinned by the conformance vectors (conformance/vectors.json,conformance/reference-impl.test.js). - Canonical JSON: recursively sorted keys, UTF-8,
undefinedencoded asnull, and no NaN or Infinity. Signatures are ES256 over the canonical text. - Record identity. A record's
vandtypenever change meaning. - Refusals. No refusal in
/cannot.jsonis removed, reused or weakened. See change control below. - Codes. No code in
/signals.jsonis removed, reused, or moved to another HTTP status class (server/signals.test.js). - The raw-data wall only grows. A key refused today is refused in every later version.
Additive only#
These may grow in any minor version; nothing that exists is taken away:
- New record blocks. Each appears only when its feature is used, so a record that does not use one signs exactly the bytes it signed before. Examples:
anchor,resolution,physical,origin,price_basis,implementations,asserts,cargo,route. - New leg blocks. They are bound into the leg signature only when present (
fine,physical,dimensions,origin,basis_commitment,bander). A leg without them has byte-identical envelope bytes. - New routes, codes, refusals and witness classes.
Experimental, for now#
These are labelled experimental until 2027-03-31. Their field sets may change with 90 days' notice:
- path certificates and ghost-coverage certificates;
- citation corridors;
- services legs;
- the terminal commitment bulletin format;
- the corridor graph. It is also not publishable until an external review of its mechanism is named (see
MATH.md); - webhook subscriptions and the delivery envelope (
mirror.webhook-delivery). The subscription and delivery ids are frozen: SPEC §8.2; - key delegation certificates (
mirror.key-delegation) and their scope names; - the signed time (
mirror.time), the audit export line schema (mirror.siem-event/v1) and the signed roadmap (mirror.roadmap); - the key-rotation plan and rehearsal records, and the incident-support pack.
Leg roles: an open namespace with defined unknown-role behaviour#
Roles are lowercase strings, not a closed enum, because the roadmap needs insurer, broker, logistics, beneficiary_bank and more:
- An unknown role is compared like any other role.
- It is required only when a policy's
required_rolesnames it. - It never gains independence from its name. A leg's class comes from the ratified key that signed it (§7.2 of the spec), never from what the role says.
So adding a role can degrade only to defined behaviour, never to a parse failure or a silent clear.
Idempotency and replay#
- A retry (identical legs) produces a byte-identical record and is counted once. The ledger keeps every submission, and every count reads distinct canonical records (
kernel/mirror-idempotency.test.js). - An amendment (any leg changed) produces a new verdict. The earlier one is never erased.
- A table version changing mid-flight gives legs banded under different tables different corridor commitments. The result is
undetermined, never a silent clear. - A failover resubmission (the same commitments re-signed by another bander host) is the same judgement. Counts and webhook delivery ids leave signature bytes out, so it is counted once and pushed once (
server/bander-ha.test.js). - A verdict the store did not append still comes back signed, with
ledger: "NOT appended ...". Resubmitting the same legs appends it as the same verdict. - A re-requested verdict is the same record under a fresh low-S signature. Signatures are canonical: a high-S copy is a legacy record or a malleated copy, never a second verdict (
kernel/mirror-malleability.test.js). - 503 responses (
no_key,ledger_unreadable,archive_failed,store_unwritable) are safe to retry with backoff.501 webhooks_unavailableis a capability the operator lacks; retrying will not help.
Change control over the refusal list#
/cannot.json is versioned:
- Adding a refusal is a minor version.
- Removing or weakening one needs a major version, a dated changelog entry, and at least 90 days' published notice. Weakening means a structural refusal becoming merely enforced, or a wire code changing.
server/cannot.test.jsholds the registry against the previously published one on every build. A silent weakening fails before it ships.
Deprecation#
- Additive features: at least 90 days' notice, dated here and on the changelog.
- Frozen items: a new major version, announced 180 days ahead.
- Plaintext join keys are the live example. They are accepted, and flagged on every record, until the published cutover of 2026-12-01. After that they are refused with
plaintext_join_key.
What this policy does not do#
It binds this codebase, and each promise is pinned by a test. It does not prove that a particular deployment runs this code. For that, compare the spec pin on /health with the published conformance-suite hash (/conformance-suite.json).