Keep a consumer bound where its data is; only a pin moves it (hq ADR 0232, issue 273)

Issue 258's fix let a mesh seat's holder elsewhere answer before this machine's own provider. Right
for the resolver, which any provider answers alike; for the store's seat it re-bound every database
consumer on a machine running its own store to the holder on another, each was given a fresh, empty
database there, and nothing said so for twenty hours.

- An offer says whether it keeps its consumers' data (`keeps-consumer-data`); unsaid, a provider
  that grants each consumer a credential does. For such a provision the seat's holder no longer
  overrules a provider beside the consumer; a pin still does.
- Where each such consumer was sent is recorded (migration 0071). A resolution that would bind it
  elsewhere keeps the recorded provider and says the move; one whose provider is gone is refused,
  never answered by another.
- A push says a kept move and raises it as an urgent condition at once; the self-check's D12 raises
  it every run, with a pinned move not yet sent as a warning and any unasked move as urgent.
This commit is contained in:
jochen
2026-10-06 15:19:23 +02:00
parent 4f4d365360
commit 8bfaf1523e
14 changed files with 1072 additions and 9 deletions
@@ -0,0 +1,31 @@
-- Where each consumer of a provision that keeps its data was bound (novox/hq ADR 0232, issue 273).
--
-- A consumer of a database is bound to its rows. What resolving chooses — the seat's holder, a pin,
-- the providers assigned where — can change under a consumer without anybody meaning to move it, and
-- on 2026-10-05 one change to how a seat's holder answers re-bound five database consumers on one
-- machine to the store on another: each was made a fresh, empty database there, and nothing warned
-- for twenty hours. The pair secrets were the only trace, and only because a new pair was minted.
--
-- One row per consumer and provision, written when a declaration carrying the binding is sent. A
-- resolution that would answer the consumer from another provider keeps this one and raises an urgent
-- condition; only a pin naming the other provider moves it, and the send that carries the move
-- rewrites the row, keeping where it was in `moved_from`.
--
-- **The provider by name, not by reference.** A provider machine leaving the mesh must not take the
-- record of where a consumer's data is with it: the data is still there, and a cascade would turn
-- the record into nothing, which resolves as a binding never made — the silent move again.
--
-- Numbered 0071, past 0070, the highest on main or any open branch when this was written.
create table binding (
node uuid not null references node(id) on delete cascade,
consumer text not null,
provision text not null,
provider_node text not null,
provider_module text not null,
-- When it was first bound where it is, and when a declaration last carried it.
bound_at timestamptz not null default now(),
sent_at timestamptz not null default now(),
-- Where it was before a pin moved it, as `<node>/<module>`; null for a binding never moved.
moved_from text,
primary key (node, consumer, provision)
);