The architecture 0117 opened needs a module to offer a service as a role on the bus — one holder, addressed by what it does. A closed table in the controller cannot express that: a capability a module contributes would require changing the mesh itself. But 0110 closed the set for a good reason — nothing could say what seats a mesh had, and the hand count came out at eleven of thirteen. That argues for enumerable, not hardcoded, and 0110 weighed free-form against a fixed table without considering a third option: closed at any moment and derived from the catalogue. A derived list cannot drift, which is how the count broke. So: the mesh's seats stay the mesh's, reserved by the mesh- prefix so the prefix is the rule and there is no list to maintain; ten seats are renamed to restore 0079's convention; everything 0110 decided about what a seat IS survives untouched. Design 29 carries the declaration model: three namespaces, subjects derived from local names so a manifest survives the wire changing, queues never declared, five relationships (the job and state shapes 0041 had no room for), and the build-publish-deploy lifecycle with hard, soft and build-time dependencies distinguished. 0041 gets a progressive insight: "no per-consumer setup, only a subscription" was a fact about a topic exchange, and a JetStream durable consumer is a real object someone creates. WBS 1.3/1.4 were wrong and say so: streams come at registration and consumers at assignment, so only the foundation set belongs at genesis.
221 lines
16 KiB
Markdown
221 lines
16 KiB
Markdown
---
|
|
topic: what runs on it
|
|
status: superseded
|
|
superseded-by: 02-DECISIONS/0118-a-module-declares-its-own-seats.md
|
|
date: 2026-09-25
|
|
deciders: jochen
|
|
reconstructed: false
|
|
extends: 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](0009-modules-and-the-graph.md) introduced claims: a module declares something
|
|
singular, at a scope, and a second holder is refused. [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md)
|
|
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](0069-a-module-is-a-repository-and-a-path.md)), 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](0109-a-package-registry-seat-is-one-per-ecosystem.md)
|
|
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](0084-which-provider-serves-a-consumer.md)
|
|
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](0009-modules-and-the-graph.md)'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](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md) 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](0085-a-secret-is-a-provision.md), 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](0079-the-foundation-seats-are-named-after-their-servers.md) |
|
|
| `mesh-store` | mesh | — | `postgres` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
|
|
| `mesh-broker` | mesh | — | `lavinmq` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
|
|
| `mesh-vault` | mesh | `secret`, reserved | `mesh-vault` | this record, for [issue 106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md) |
|
|
| `the-artifact-store` | mesh | `artifact-store` | `distribution` | [0075](0075-two-stores-and-which-provides-what.md) |
|
|
| `the-catalogue` | mesh | — | `mesh-catalog` | this record |
|
|
| `npm-package-registry` | mesh | `npm-package-registry` | `gitea` | [0109](0109-a-package-registry-seat-is-one-per-ecosystem.md) |
|
|
| `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](0109-a-package-registry-seat-is-one-per-ecosystem.md)'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](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md). The vault is one
|
|
per mesh ([ADR 0085](0085-a-secret-is-a-provision.md), 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](0009-modules-and-the-graph.md): a claim in a definition says a module *can* hold a seat;
|
|
the assignment says it does.
|
|
- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md) and
|
|
[ADR 0078](0078-the-store-and-broker-are-modules.md): "a mesh runs one postgres and one
|
|
lavinmq" becomes one holder of `mesh-store` and one of `mesh-broker`. The store and broker modules may
|
|
run on other nodes.
|
|
- [ADR 0084](0084-which-provider-serves-a-consumer.md): 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](0109-a-package-registry-seat-is-one-per-ecosystem.md): one provision per package
|
|
ecosystem stands. Where 0109 says *seat*, it means that provision. Only `npm-package-registry` is
|
|
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 "assigning `npm-package-registry` to
|
|
verdaccio". It takes verdaccio's definition saying it can hold the seat, and then an assignment
|
|
holding it.
|
|
- [To-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md): the same two changes, in the design
|
|
that describes choosing a provider.
|
|
- [To-be 21](../03-DESIGN/01-to-be/21-the-installation-in-full.md): 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 `seats` command 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](0089-a-bed-reads-the-catalogue-it-proves.md)). |
|
|
| 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](0009-modules-and-the-graph.md): claims, scopes, and "refused, never guessed"
|
|
- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md): a seat named after what it is
|
|
- [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md): the per-ecosystem registry seats
|
|
- [ADR 0084](0084-which-provider-serves-a-consumer.md), [to-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md):
|
|
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)
|