16 KiB
topic, status, date, deciders, reconstructed, extends
| topic | status | date | deciders | reconstructed | extends |
|---|---|---|---|---|---|
| what runs on it | proposed | 2026-09-25 | jochen | false | 0009-modules-and-the-graph.md |
110. A seat is held by one 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 four 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.
A claim in a definition makes a module singular, not a role. The store module's definition
claims mesh-store, so every assignment of it claims the seat, and a second store module on any other
node is refused. What is singular is the store the mesh itself uses, not postgres. Any module can
run on any node whose capabilities match, which is a core principle of the module system, and a claim
written into the definition breaks it for every module that claims anything.
Some seats are the mesh's one of something everyone consumes, and nothing uses that fact. 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, claimed in definitions. Rejected. The overview stays unanswerable, a module that claims a seat can run on only one node, and a second provider of anything costs a pin per consumer node.
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 held by one assignment, from a closed set, and holding it may deliver a provision. Chosen.
Decision
The mesh defines a closed set of seats. Each entry has a name, a scope, what holding it delivers (if anything), and the decision that made it a seat. A seat outside the set is refused wherever it is named. 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 definition says which seats a module can hold. An assignment says which it does hold. The
store module can hold mesh-store, and it may be assigned to every node. Exactly one of those
assignments holds the seat, because that assignment said so. A second assignment saying so, at the
seat's scope, is refused. So a seat makes a role singular, never a module, and moving the role is
changing which assignment holds it, with no definition changed and nothing unassigned.
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.
Holding a seat may deliver a provision. A seat that delivers a provision may only be held by an assignment of a module that provides it, at the seat's scope.
A requirement may name a seat, and then the seat's holder answers it. Naming the seat asks for
the mesh's one, not for whichever provider is nearest, so the holder answers even when another
provider runs on the consumer's own node, and nothing is asked of anyone. Unheld, the requirement is
refused, naming the seat. A builder asks for the mesh's npm registry this way, and is served by the
holder of npm-package-registry wherever it runs, with no pin on any machine.
A requirement that names no seat resolves as ADR 0084 has it: a pin, then the provider on the consumer's own node, then the only provider. Where several remain and none is local, a person chooses when the module is assigned. Assignment lists the candidates, with the holder of a seat that delivers the provision suggested first, and records the answer on the assignment as its pin. Without an answer the module is not assigned. This keeps ADR 0009's rule that a requirement with several answers is never guessed. The choice is made either by the requirement naming the seat, or by a person at assignment, and never silently by what happens to run nearby. That is the failure issue 106 names for the vault.
A seat delivers a provision only where the mesh has one answer for everyone. 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 foundation's seats deliver nothing. mesh-controller, mesh-store and mesh-broker name
which assignment the mesh itself uses: the controller, the store holding its records, the broker
carrying its bus. The store and broker modules may run on other nodes too, and a database or amqp
consumer that names no seat is served by co-location from whichever runs on its own node, the seat's
holder included. Were mesh-store to deliver, a consumer could name it and be sent to the store the
mesh keeps its own records in. That is not a store for consumers.
A seat may reserve its provision. Where a second provider would break the reason the provision
exists, only an assignment holding the seat may provide it at all: the parser refuses a definition
that provides it without being able to hold the seat, resolution refuses an assignment providing it
without holding the seat, and a pin cannot choose anyone else: a requirement for it always names the seat. secret is the one
reserved provision.
The vault is one per mesh because a second "would be a second place to lose"
(ADR 0085, as amended), and a second secret provider is exactly
that, whether a pin chose it or not.
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.
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 definition is refused by it:
| seat | scope | delivers | can be held 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, reserved |
mesh-vault |
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.
A gitea assignment holds it. verdaccio provides the same provision and cannot hold the seat, so it is
the second provider this record exists to make harmless. Moving npm to it would take a definition
saying it can hold the seat, and then an assignment saying it does.
mesh-vault answers issue 106. The vault is one
per mesh (ADR 0085, as amended), and until now that was enforced by
nothing. The seat is named after its server, by the 0079 convention.
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.
What this changes in earlier records
On acceptance, each of these is amended by this record, not edited:
- ADR 0009: a claim in a definition says a module can hold a seat; the assignment says it does.
- ADR 0079 and
ADR 0078: "a mesh runs one postgres and one
lavinmq" becomes one holder of
mesh-storeand one ofmesh-broker. The store and broker modules may run on other nodes. - ADR 0084: a requirement may name a seat, which its holder answers; and where several providers remain and none is local, the choice is asked when the module is assigned and recorded as a pin, rather than refused until someone pins it.
- ADR 0109: one provision per package
ecosystem stands. Where 0109 says seat, it means that provision. Only
npm-package-registryis also a seat in this set. A cargo or docker registry becomes one by a record, as any seat does. "Gitea may hold several seats" reads: gitea may provide several ecosystems, and hold the seat of each one that is a seat. Moving npm to verdaccio is not "assigningnpm-package-registryto verdaccio". It takes verdaccio's definition saying it can hold the seat, and then an assignment holding it. - To-be 23: the same two changes, in the design that describes choosing a provider.
- To-be 21: genesis assigns the foundation's store, broker and controller holding their seats, where their definitions claim them today.
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.
- An assignment gains the seats it holds. Genesis assigns the foundation's store, broker and controller holding their seats, where today their definitions claim them.
- Manifest validation refuses an unknown seat, a seat named at the wrong scope, and a delivering seat named by a module that does not provide the provision. Resolution refuses a second holder, and an assignment holding a seat its module cannot hold.
- Resolution answers a requirement naming a seat with its holder. Assignment asks a person where several providers remain, suggesting the seat's holder first, and records the answer as a pin. A provider record gains the module it came from.
- A
seatscommand lists the set with each seat's holder, derived from assignments. - What got harder: a module wanting a new singular role can no longer invent a name. It needs a record. And an assignment has one more thing to say. Both are the point.
- Not changed: the controller's seat placeholder stays as it is. It exists so the controller can reach a foundation it made before any module existed.
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 seat outside the set is refused | Manifest-validation tests for an unknown seat, the wrong scope, and a delivering seat named by a module that does not provide it. |
| Every module in use names a seat in the set | A controller test parses every catalogue manifest and fails on any refused seat. The lab's beds read the same manifests (ADR 0089). |
| A seat is held by one assignment, not by a module | A resolution test: the store module assigned to two nodes resolves, with one assignment holding mesh-store; a second assignment asking to hold it is refused. |
| A requirement naming a seat is answered by its holder | Resolution tests: two providers with the seat held; a second provider on the consumer's own node, where the holder still answers; the seat unheld with two providers, and with one provider, both refused naming the seat. |
| An assignment holds only a seat its module can hold | A resolution test: an assignment holding a seat its definition does not name is refused. |
| Every seat is listed with its holder | A seats command test: every seat in the set is listed with its scope, what it delivers and its holder, and an unheld seat is listed as unheld. |
| Several providers and none local is a person's choice | An assignment test: the candidates are listed with the delivering seat's holder first; the answer is recorded as a pin; with no answer the module is not assigned. |
| The foundation's seats route nobody | A resolution test: with the store module on two nodes, a database consumer is served by the one on its own node, whichever holds mesh-store; a requirement naming mesh-store is refused, because it delivers nothing. |
| A reserved provision has no other provider | The parser refuses a definition providing secret that cannot hold mesh-vault; resolution refuses an assignment providing it without holding the seat, and a pin on a secret requirement. |
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
- ADR 0084, to-be 23: pins, co-location and refusal
mesh-controller internal/catalogue/resolve.go(checkClaims, the brokered branch ofResolve),internal/catalogue/manifest.go(claim validation),cmd/mesh-controller/plan.go(holdings)