Decided with the author: - A seat is held by one assignment, not claimed by a definition. A definition says which seats a module can hold; an assignment says which it does. The store module can run on every node and one assignment holds mesh-store; moving a role changes an assignment, never a definition. The foundation's seats name what the mesh itself uses and route no consumer — database and amqp consumers use co-location, the holder included. This replaces the wrong rationale that the foundation's store is "provider to nobody", which contradicted ADR 0078 and to-be 21. 0079's one-postgres rule becomes one mesh-store holder. - A module is assigned at most once to a node. The instance identity in 0112 and 27 is withdrawn, and the login-length problem with it. Review fixes to 0113: - The bottom of the stack: the vault is installed as soon as the shared runtime base exists, and genesis generates everything needed until then — including the permanent controller's, the control-node agent's, the builder's and the broker provisioner's bus accounts, and the controller's store login. Genesis creates those accounts until the broker's provisioner runs and adopts them. - Genesis's values are delivered recorded as the mesh's own, so 0092's never-replace rule for operator values does not make them unrotatable. - Backend-issued secrets (a forge's once-only API token) enter through the vault. Non-module parties (the controller's logins, node agents' accounts) are answered the same way, the controller asking on their behalf; an enrolment token reaches the controller only as what verifies it. - A secret with no provisioner to apply it is marked not rotatable by the mesh and refused, instead of a restart reported as done. Unused password generators in six provider clients are removed, and a catalogue scan checks no module mints. - Rotation's lock-out cases (offline reader, bus account owner, restarted provisioner) are recorded as open, with overlap and re-confirm-with-safeguards as the two answers, to be chosen before acceptance. 0110, 0111 and 26 are marked proposed: they changed in meaning and are under review, and an accepted record must not rest on proposed ones. To-be 23 and the glossary are restored to main; they change when these records are accepted.
13 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 for that provision resolves, in order, to:
- a provider the consumer's node was pinned to, a consumer coupled to one provider's contents;
- the holder of the seat, even when another provider runs on the consumer's own node;
- otherwise refused, naming the unheld seat.
Co-location does not apply to a provision a seat delivers. 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 the choice made once, mesh-wide, by assigning the holder, instead of once per consumer by pinning.
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 is served by co-location from whichever runs on its own node, the seat's holder included.
Were mesh-store to deliver, every database consumer on every node would be sent to one machine.
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. 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: "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.
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 prefers the seat's holder for a provision it delivers, after 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. |
| 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 node, where the holder still answers; the seat unheld with two providers, and with one provider, both refused naming the seat. |
| 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 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)