ADR-0054 — Actor-registry federation: admit-and-dispute, the signed actor-event wire shape, and the adjudication algebra¶
- Status: Accepted (refines ADR-0011, ADR-0044, ADR-0046; applies ADR-0015/ADR-0040/ADR-0045/ADR-0017/ADR-0018/ADR-0027; the interim-attribution surface extends ADR-0007's consumer side)
- Date: 2026-07-19
Context¶
ADR-0011 §4 promises partition-safe
enrolment — "enroll locally during an outage; the event syncs upstream like any other."
ADR-0044/ADR-0046
then made enroll_actor fail closed bidirectionally (≤ 1 key per actor_id, ≤ 1 actor_id per
key, whole-history). Two partitioned nodes can each locally-validly create what is jointly a
violation:
- A-direction: two different people whose pinned sets are degenerately identical → the same
actor_idunder two keys (each node saw only one enrolment, so neither refusal fired). - B-direction: one signing key under two
actor_ids — most commonly one clinician enrolled at two facilities during a partition whose nodes minted slightly divergent pinned sets. (Identical pinned sets compute the sameactor_idand dedupe harmlessly; only divergence conflicts.)
On reconnect, set-union delivers both signed events. A sync-apply door that mirrors the enroll
checks as refusals refuses signed history — violating lossless custody
(sync §6.3/§6.5, "never reject, never drop") — while
accepting both into today's actor_current (DISTINCT ON (actor_id), latest wins) recreates the
silent merge / attribution loss the guards exist to prevent. No prior ADR specified the
resolution (the 2026-07-15 review, finding C4 / issue #205). Meanwhile the attestation and
suppression owner-gates trust only the local registry (issue #154), so a human's vouch is not
portable across nodes at all — and everything accountability-shaped now routes through
actor_current.
Two facts sharpen the design. First, the threat model is benign-dominant: the common conflict
producer is provisioning sloppiness or the locum-at-two-sites case; hostile key-reuse is the rare
tail. So the mechanism must make the benign case cheap to adjudicate while never auto-resolving
either case (the ADR-0014 posture: uncertainty may
only withhold; a hard conflict forces a human decision, never an auto-pick). Second, today's
actor_event rows are not wire events at all — no signature, no content address, no HLC, no
origin node — and actor_current orders by node-local (recorded_at, seq), which cannot converge
across nodes even without conflicts. The wire shape is therefore a day-one, can't-retrofit
obligation of this ADR (the ADR-0042 pattern).
Decision¶
1. The actor event becomes a first-class signed wire event (day-one shape).
COSE_Sign1 (ADR-0015) under a
new dedicated signing context for the actor plane
(ADR-0040 domain separation — registry bytes can never
replay as clinical or node events). Signer = the enrolling node (the ceremony authority — the
ADR-0053 {node, recorded} posture); the enrollee's public
key, pinned set, any human co-signature, and the ADR-0044
person-distinguishing determinant are content. Content-addressed (multihash of the signed
bytes — the wire dedupe/idempotence key). HLC-stamped (t_recorded, graded per
ADR-0027) with origin node_id. The cross-node winner order for
actor_current becomes (HLC, content_address) — deterministic, clock-collision-proof,
collation-independent (ADR-0045; this
winner-rule change is itself ADR-gated per sync §6.1, and this is that
ADR). Local seq survives only as the intra-node tiebreak for pre-wire local rows. The actor_id
derivation (content-address of the pinned set alone) is unchanged. Pre-wire unsigned rows
never sync — the apply door refuses unsigned registry history; dev/PoC rigs are wiped, never
synced through (the ADR-0051/ADR-0052
posture; we are pre-clinical, so this is stated discipline, not migration).
2. Transport: the actor stream rides the node plane.
Deny-all, trusted-peer admission (ADR-0017):
registry trust is node trust — a node-signed enrolment is inherited exactly as far as the peer
is trusted, and a peer that turns out bad is answered by the
ADR-0018 revocation cascade. The
registry is small trust-plane state, so it replicates fully within the trust neighborhood —
which is what makes a human's vouch portable. Per-peer seq cursor and the existing node-plane
quarantine pen (re-offer floor, acked human exclusion — db/022's contract) are reused verbatim
for unverifiable bytes.
3. The apply door admits, then detects — never refuses verifiable history.
apply_remote_actor_event: verify signature + signing context (unverifiable → pen) → origin
admission (untrusted → skip-and-sweep: not applied now, revisited by a background sweep when peer
admission changes — never silently dropped) → content-address dedupe (idempotent) → INSERT
unconditionally (custody is total) → re-evaluate the derived dispute state (point 4) and
raise a salience-routed worklist item (identity §5.12)
on a newly-disputed actor_id/key — at both implicated nodes, never a modal (principle 3). The
local enroll_actor door keeps its fail-closed whole-history stance unchanged; loud-at-creation
remains the first line. Admission ≠ endorsement: safety lives in the projection.
4. Disputed is derived, never declared — and computed over live bindings.
An actor_id/key is disputed iff the conflict predicates detect a violation among live
bindings in the local log — a binding is live until a later algebra event closes it (supersede,
revoke, or rotate-key rotating the key away; without the rotate-key closer, every routine
rotation would read as a false two-key dispute): ≥ 2 live keys under one actor_id, or
≥ 2 live actor_ids under one key. No event creates or clears the dispute; adjudication changes
the log the state derives from (ADR-0010
derived-not-declared). Consequence: log convergence ⇒ state convergence — no dispute flag to
race, no "who noticed first." Projection surface: actor_current stops picking winners for a
disputed actor_id; a sibling projection (actor_registry_state) carries
clean | disputed(candidate_set) | revoked per actor_id and per key, enumerating the conflicting
rows. The predicate split is deliberate and load-bearing: the derived state uses live
bindings so adjudication clears it by construction; the enroll-door predicates stay whole-history
(anti-resurrection, fail-closed). Without the split, the A-case repair deadlocks — re-enrolling a
key under its proper successor would trip the whole-history B-check; supersede is the sanctioned
repair path the doors recognize.
5. Content never waits; permissions always wait.
No strict cross-plane ordering is promised (registry and clinical streams have independent
cursors; promising more would lie at the first partial sync). Instead, at the consuming doors:
(a) a clinical event citing a key the local registry doesn't know yet, or one in dispute,
applies normally — availability over consistency — with attribution honestly degraded: unknown
key → "key not yet resolved," disputed key → the candidate set ("one of Dr X / Dr Y — registry
dispute pending"), a first-class identity §5.10
authorship state (db/005's NULL-on-ambiguity survives only as the fallback the candidate set
cannot express). (b) An operation where the registry grants authority — a suppression's
owner-gate, an attestation's authority check — is penned (delayed-never-lost, re-offered) until
the cited registry state arrives and is clean; disputed = not clean = withheld. Fail-safe by
direction: a held suppression keeps the note visible; a held attestation reads un-vouched —
never the reverse. A hostile fabricated enrolment therefore buys a worklist entry and zero
authority. This rule — registry uncertainty may withhold a permission, never withhold content —
is principle 4 operationalized, and it closes issue #154 structurally. Attribution re-derives
retroactively and losslessly the moment the registry catches up or the dispute resolves; the
dispute window stays auditable in the log forever. (In the A-direction, per-event attribution is
unambiguous by signing key the whole time — only the registry identity of each key is disputed —
so the candidate set is temporary by construction; genuine permanent ambiguity exists only in the
stolen-key tail, resolved by revoke + the ADR-0011
§6 contamination cascade, unchanged.)
6. Adjudication is supersede by audited human ceremony — no new verbs.
- B-case (join): two
supersedeevents, each oldactor_id→ one successor (corrected pinned set, proper determinants). A join expressed as links — never a merge; both lineages survive. - A-case (per-key fork): the degenerate binding is superseded per key; each person gets a
successor
actor_id(distinguishing determinants added) binding their key. Attribution then re-derives exactly, per signing key. (The supersede content therefore cites the specific enrolment binding it closes, never anactor_idwholesale — an additive wire-shape field, canonical in data-model §3.12.) - Hostile tail:
revokewith compromise-time + contamination cascade.
The ceremony is audited with a mandatory recorded human adjudicator (the ADR-0011 §4 backstop); who may adjudicate is policy (principle 9). Adjudication events sync as ordinary actor events. If two nodes adjudicate the same dispute differently during another partition, the live-binding derivation sees conflicting supersedes and the state honestly reads disputed again, worklist escalated — never auto-picked. Rare, honest, convergent.
7. Considered and rejected.
- Pen-outside + adjudicated admission (mirror the checks as refusals into a quarantine pen): registries diverge until adjudication — each node holds its own enrolment and pens the other's — so the conflict has no convergent representation; the adjudication event must cite bytes outside the log; candidate-set attribution cannot be computed legibly from a pen row; and the pen was built for unverifiable bytes, not unwelcome verifiable history — stretching it there is exactly the "reject signed history" the sync contract forbids.
- Deterministic tiebreak + auto-supersede (HLC/content-address picks a winner): an automated identity resolution — the move principle 2 and ADR-0014 forbid. A tiebreak that silently hands Dr X's authorship to Dr Y because of a clock value is the failure mode this project exists to refuse.
Canonical homes: security §7.5 (registry federation + dispute + adjudication), sync §6.9 (the actor stream + ordering contract), data-model §3.12 (wire shape + convergent ordering), identity §5.10 (candidate-set authorship state).
Consequences¶
- Easier: the C4 contradiction dissolves — custody is total (both signed events, every trusted node, byte-for-byte), "never reject" holds, and the silent merge is unreachable because no projection picks a winner under dispute. A human's enrolment/vouch becomes portable (closes #154 structurally). The sync-door half of the #172 mirror obligation is discharged by specification (the door runs detection, not refusal — deliberately). Log convergence ⇒ dispute-state convergence, for free.
- Harder:
actor_currentand every accountability-shaped consumer must learn the disputed state — a real projection blast radius. The "≤ 1 key peractor_id" invariant becomes a steady-state projection claim, not a table invariant. A second predicate family (live-bindings) joins the whole-history one; the split must stay legible or a future door will call the wrong one. - Operational: pre-wire unsigned
actor_eventrows never sync — wipe dev/PoC rigs before federating them. Therotate-key/supersedelocal door obligation (#172's other half) is restated with one qualification: it must mirror both whole-history checks applied lineage-aware — a key may re-bind only along its own supersede lineage. (Lineage-blind mirroring would refuse every key-preserving supersession and deadlock the point-6 repairs; lineage-aware, the forks and joins pass while a foreign key-grab still refuses.) The human key-loss recovery ceremony remains an open gap needing its own ADR before any pilot enrolls real humans (security §7.5, issue #94); nothing here forecloses it. - Blast radius (§9): the wire shape, apply door, dispute derivation, and adjudication ceremony are safety-critical (in-DB/Rust, reviewer-legible, TDD). The dispute derivation is a pure function of the log — property-test that admission order never changes it (set-union commutativity + idempotence over arbitrary interleavings), plus multi-node convergence (partition → dual enrol → reconnect → both disputed → adjudicate once → both clean).
- The bet: that a derived, live-bindings disputed state plus supersede-only repair absorbs every registry conflict a partition can mint — with no new verbs and no auto-resolution. We would know it is wrong if a real conflict class cannot be expressed as live-binding supersedes, if the dispute state flaps pathologically under normal provisioning, or if withheld-while-disputed permissions block a clinically-urgent workflow (which would indicate the permission, not the dispute, is mis-scoped).
- No new founding principle. This is principles 1, 2, and 4 applied to the actor registry: append-only custody, never-merge/always-link, and acknowledged uncertainty — now at the trust anchor itself.