Files
hq/02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
T
jochen 6e3373c879 Design pass: address the review
0113 — the plaintext claim was false under its own mechanism: handing a provider's answer to the
controller puts every secret on the broker and in the controller in the clear. The provider now seals
each secret field itself, to the consumer node's public key the mesh hands it, and the controller
carries sealed fields it cannot open. That is stricter than today, where the controller holds every
minted credential in the clear. Option 3 (plaintext to the controller) is recorded and rejected. The
foundation exception now covers root-secret rotation (0085) and forms like the broker admin's hash, so
no phase claims to remove the broker's bootstrap step. To-be 24 and 13 are named among what it amends.

27 — resolution is consistent with 0110: co-location and the only provider apply only where no seat
delivers the provision, so an unheld seat is refused even with one provider. The secret-field rule now
matches 0086 exactly (a declared env-file, never a container environment value). The seat placeholder
is the controller's, and the one module reading it moves to a host port. Contracts are held by the
controller and written down in phase 1, so they can be checked; every rule has a check. An operator's
secret is still the operator's, with the vault as custodian. Which seats a module holds is listed as
not settled.

0110 — the unheld-seat-with-one-provider case and the one-answer-for-everyone rule have checks; the
claim about moved manifests is corrected. 26 — the table governs and the code catches up, not the
reverse; scope and capacity agree with the glossary; moving a seat is described as it really is today.
0112 — aligned with 27, and lists 0049 and 26 among what it changes.

Issue 118 is renumbered 119: another branch took 118 first. 'Control-plane' is gone from 0110 and 0111.
2026-09-25 22:46:10 +02:00

12 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
what runs on it accepted 2026-09-25 jochen false 0009-modules-and-the-graph.md

110. A seat is a module assignment from a closed set, and it may deliver a provision

Context

ADR 0009 introduced claims: a module declares something singular, at a scope, and a second holder is refused. ADR 0079 named the foundation's three after their servers. That mechanism is enforced and works. What it means has drifted, and three things are now true of it that no record says.

Any well-formed name becomes a seat by being claimed. The controller's manifest check refuses a claim only for a malformed name or an unknown scope. Nothing says which seats a mesh has. The names in use were each invented by the module that claims them: the-showcase, the-build-machine, the-intrusion-prevention.

Nothing can say what a mesh has, or who fills it. There is no seat table and no command that lists seats. Holdings are assembled while planning, one node at a time, and discarded afterwards. The only way to answer "which seats does this mesh have, and which module holds each" is to read every manifest in two repositories, because the controller's own manifest lives in its own repository (ADR 0069), and then the controller's code, because one module it ships has its manifest composed there. While this record was being prepared, that enumeration was done by hand, and it missed both of the last two sources: eleven claims were reported where there are thirteen.

Some seats are the mesh's one of something that others consume, and nothing uses that fact. Of the thirteen claims, four are held by a module that provides something consumers require: mesh-store (postgres-database, eleven consumers), mesh-broker (amqp), the-artifact-store (artifact-store) and the-dns-port. The rest deliver nothing to anybody, and are still meaningful: they say which module is this mesh's packet filter, or resolver configuration. Meanwhile a requirement for a mesh-scoped provision with more than one provider is refused until a person pins, per consumer node, which provider to use. ADR 0109 anticipates exactly that case — gitea and verdaccio both answering npm — and under today's resolution it would mean a pin on every machine that builds anything.

Considered Options

1. Leave seats as free-form exclusion, and keep provisions as the only thing that delivers. Rejected. The overview stays unanswerable, and a second provider of anything costs a pin per consumer node — the decision "gitea is our npm registry" made again on every machine.

2. Two concepts: seats for exclusion, and a new word for the mesh's one consumable thing. Rejected. Both mean "this mesh's one X". Every existing claim would first have to be classified into one or the other, and the overview a person wants is one list, not two.

3. A seat is a module assignment from a closed set, and occupying it may deliver a provision. Chosen.

Decision

The mesh defines a closed set of seats. Each entry has a name, a scope, what occupying it delivers (if anything), and the decision that made it a seat. A claim naming a seat outside the set is refused, and so is a claim at a scope other than the seat's. Adding a seat is a decision, for the same reason adding a shape to the host's vocabulary is one: the set is what a person reads to learn what a mesh can have, and a name added without an argument is a name nobody can explain later.

A seat is held by a module assignment. What the mesh knows about a seat's holder is what it knows about that assignment: its node, the node's settings for it, and what it serves. Holdings are not stored separately. The seat points at an assignment, and a second record of the same fact would be a second thing to disagree with the first.

A seat may deliver a provision, and then its holder answers for it. A seat that delivers a provision can only be held by a module that provides it, at the seat's scope, and a claim that does not is refused. A requirement for that provision resolves, in order, to:

  1. a provider the consumer's node was pinned to — a consumer coupled to one provider's contents, as to-be 23 already allows;
  2. the holder of the seat that delivers it, even when another provider runs on the consumer's own node;
  3. otherwise refused, naming the unheld seat.

Co-location does not apply to a provision a seat delivers. For every other provision, a provider on the consumer's own node answers first, then the only provider, then refusal (to-be 23). A seat exists to say which one is the mesh's, and co-location answering first would let any second provider on a consumer's machine take over silently for that consumer. That is the failure issue 106 names for the vault.

This keeps ADR 0009's rule that a requirement with several answers is never guessed. The seat is not a guess. It is the choice made once, mesh-wide, by assigning the holder, instead of once per consumer by pinning. A second provider may run beside the holder, and whatever requires the provision still resolves to the holder without anybody naming it.

Seats are also informational. The controller lists every seat in the set, what it delivers, and which assignment holds it, including seats nobody holds. An unheld seat is an answer, "this mesh has no X", not an error.

A seat delivers a provision only where the mesh has one answer for everyone. That is a design decision about the provision, not about the seat. The artifact store, the npm registry, git and the vault are each one per mesh by their own records, so their seats deliver them. The store and the broker are not: to-be 23 has each node running its own stores, with a consumer served by the one on its own machine. So mesh-store and mesh-broker keep guarding that the foundation's own server is singular, and deliver nothing. Were they to deliver, every database consumer on every node would be sent to the control-node's store.

The first set is the twelve seats already claimed, plus two. Thirteen claims are in use, and they name twelve seats because two alternative modules claim the-resolver-configuration. This record admits every seat the catalogue and the controller claim today, so no module is refused by it:

seat scope delivers held today by made a seat by
mesh-controller mesh — mesh-controller 0079
mesh-store mesh — postgres 0079
mesh-broker mesh — lavinmq 0079
mesh-vault mesh secret nothing yet: mesh-vault claims it this record, for issue 106
the-artifact-store mesh artifact-store distribution 0075
the-catalogue mesh — mesh-catalog this record
npm-package-registry mesh npm-package-registry gitea 0109
the-build-machine node — builder this record
the-dns-port node — dnsmasq this record
the-intrusion-prevention node — fail2ban this record
the-packet-filter node — nftables this record
the-private-network node — the controller's private-network module this record
the-resolver-configuration node — resolv-conf or resolved-split-dns this record
the-showcase node — showcase this record

There are two additions. npm-package-registry is ADR 0109's seat, named after the provision it delivers, as 0079 named the foundation's seats after what they are. gitea claims it. verdaccio provides the same provision and claims nothing, so it is the second provider this record exists to make harmless.

mesh-vault answers issue 106. The vault is one per mesh (ADR 0085, as amended), and until now that was enforced by nothing. A second vault would have answered requirements silently, and any consumer on its machine would have been served by it through co-location. The seat is named after its server, by the 0079 convention, and the vault module claims it.

the-dns-port is listed as delivering nothing, although dnsmasq provides wildcard-resolution. That provision is node-scoped and answered on the machine, so no preference between providers arises. Whether the seat should say it delivers it is left for when a second resolver makes the question real.

Consequences

  • The controller carries the set in code. A test asserts its size, and that every entry names the record that made it a seat, so changing the set means finding the argument rather than a number. This is the pattern the host's vocabulary test already follows.
  • Manifest validation refuses an unknown seat, a seat claimed at the wrong scope, and a provision-delivering seat claimed by a module that does not provide the provision. The three refusals name the seat and the set.
  • Resolution prefers the seat's holder among several providers, after a pin. A provider record gains the module it came from, because two modules on one node could otherwise not be told apart as holder and non-holder.
  • A seats command lists the set with each seat's holders, derived from assignments.
  • gitea claims npm-package-registry. The catalogue's package-registry becomes npm-package-registry throughout, per ADR 0109.
  • What got harder: a module wanting a new singular role can no longer invent a name. It needs a record. That is the point, and it costs one record per seat.
  • Not changed: ${seat:<seat>:<port>} stays as it is. It exists so the controller can reach a foundation it made before any module existed, and it cannot be a consumer. A module that needs something from a seat's holder requires the provision the seat delivers, and receives it the way any provision is received: through a grant.

How it is checked

Rule Checked by
The set is closed, and every entry names its decision A controller unit test asserts the set's size and a non-empty decision for every entry.
A claim outside the set is refused Manifest-validation tests for an unknown seat, the wrong scope, and a delivering seat whose claimant does not provide.
Every module in use claims a seat in the set A controller test parses every catalogue manifest and fails on any refused claim. The lab's beds read the same manifests (ADR 0089).
The holder answers for a provision its seat delivers Resolution tests: two providers with the seat held; a pin overriding the seat; a second provider on the consumer's own machine, where the holder still answers; the seat unheld with two providers, and with one provider, both refused naming the seat.
A seat delivers only a one-per-mesh provision A controller unit test: mesh-store and mesh-broker deliver nothing, so a database consumer is still served by co-location.

References

  • ADR 0009: claims, scopes, and "refused, never guessed"
  • ADR 0079: a seat named after what it is
  • ADR 0109: the per-ecosystem registry seats
  • to-be 23: pins, co-location and refusal
  • mesh-controller internal/catalogue/resolve.go (checkClaims, the brokered branch of Resolve), internal/catalogue/manifest.go (claim validation), cmd/mesh-controller/plan.go (holdings)