DocumentsThe protocol and its limits

Mirror — protocol stability policy

4 min readSource stability-policy.mdsha256 9dcd7f9405435b10…

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, and cleared is true only for reconciled.
  • 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, undefined encoded as null, and no NaN or Infinity. Signatures are ES256 over the canonical text.
  • Record identity. A record's v and type never change meaning.
  • Refusals. No refusal in /cannot.json is removed, reused or weakened. See change control below.
  • Codes. No code in /signals.json is 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_roles names 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_unavailable is 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.js holds 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).