Until now the holder was derived — assigned and claiming — and a second eligible assignment was refused, so a seat could not pass from one holder to the next without a moment where nobody held it. The controller finds its own bus through one of these seats, and that moment took the control plane down on 2026-09-27. The holder is now a row the controller keeps, written by `seat <name> --to <node>/<module>` in the same write that removes the previous one. No row means the old rule, so nothing changes for a mesh that never hands a seat over; with a row, another eligible assignment is silent rather than refused, which is what lets the next holder run beside the current one until the switch. A holding is the assignment's and goes when it does. Each rule names the test that checks it. Under ADR 0131; design 28 task 5.3 is the work.
246 lines
17 KiB
Markdown
246 lines
17 KiB
Markdown
---
|
|
layer: to-be
|
|
status: implemented
|
|
code:
|
|
- mesh-controller internal/catalogue/seats.go
|
|
- mesh-controller internal/catalogue/resolve.go
|
|
- mesh-controller internal/inventory/seats.go
|
|
- mesh-controller internal/inventory/migrations/0039-a-seat-is-held-by-one-assignment-on-record.sql
|
|
- mesh-controller cmd/mesh-controller/seats.go
|
|
- mesh-controller cmd/mesh-controller/source.go
|
|
- mesh-controller internal/inventory/migrations/0032-a-source-may-live-on-a-seat.sql
|
|
- mesh-catalog modules/gitea/module.json
|
|
updated: 2026-09-27
|
|
decisions:
|
|
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
|
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
|
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
|
|
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
|
|
- 02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md
|
|
- 02-DECISIONS/0117-a-machines-uplink-is-a-seat.md
|
|
---
|
|
|
|
# 26 — The seats
|
|
|
|
**What a mesh can have one of, and who fills each.** A seat is a named role at a scope, held by one
|
|
module assignment. The mesh defines which seats exist. Holding one may deliver a provision, and the
|
|
list of seats with their holders is the quickest answer to "what is in this mesh".
|
|
|
|
## What a seat is
|
|
|
|
A seat has four properties, fixed by the mesh rather than by any module:
|
|
|
|
| property | is |
|
|
|---|---|
|
|
| name | what a definition names and an assignment holds, and what a person reads in the list |
|
|
| scope | node, site or mesh: where its capacity applies. Every seat in the set has a capacity of one, so one holder per scope. A bench, a seat with several holders, is a word the glossary keeps and no seat uses yet |
|
|
| delivers | the provision its holder answers for, or nothing |
|
|
| decision | the record that made it a seat |
|
|
|
|
**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 run on every node whose capabilities match. Exactly one of
|
|
those assignments holds the seat, because that assignment says so, and a second assignment saying so
|
|
is refused. A seat makes a role singular, never a module.
|
|
|
|
**The seat points at the assignment.** Everything the mesh knows about the holder is what it knows
|
|
about that assignment: the node, the node's settings for the module, and what the module serves.
|
|
|
|
**Which assignment holds a seat is a fact on record, and changes as one act.** Revision, 2026-09-27
|
|
([ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)). Until
|
|
then the holder was derived — the module that is assigned and claims the seat holds it, and a second
|
|
eligible assignment was refused. That has no way to pass a seat from one holder to the next without a
|
|
moment in which nobody holds it, and the controller finds its own bus through one of these seats: that
|
|
moment took the control plane down for an evening. So the holder is now one row the controller keeps,
|
|
written by a handover — `seat <name> --to <node>/<module>` — that names the seat and the assignment
|
|
taking it over and replaces the previous holder in the same write. Between two handovers the seat has
|
|
exactly one holder, and it is never none.
|
|
|
|
Three consequences follow. **A seat with no row is held as it always was**: the sole eligible
|
|
assignment holds it, and two eligible ones are refused — so a mesh that has never handed a seat over
|
|
behaves exactly as before, and the row appears the first time somebody does. **With a row, any other
|
|
assignment whose module could hold the seat is eligible and silent**: neither refused nor holding.
|
|
That is what lets the next holder run beside the current one until the handover, which the bus's move
|
|
needs ([28](28-building-the-bus.md), task 5.3). **And a holding is the assignment's**: unassigning the
|
|
holder takes the row with it, so a seat never points at something that is not running anywhere, and
|
|
the seat falls back to derivation rather than to nothing.
|
|
|
|
The handover refuses what would make the new holder wrong before anything is written: the seat must
|
|
exist, the assignment must exist, and the module must be able to hold the seat — claim it at its scope
|
|
and provide what it delivers, judged against the store's row and not against anything compiled into a
|
|
binary. It does not check that the module is running yet; `push` confirms that afterwards, and a
|
|
handover that could only be recorded after the new holder was up could not be the switch.
|
|
|
|
**The set is closed.** A seat the mesh does not define is refused wherever it is named, and so is one
|
|
named at the wrong scope. Adding a seat is a decision, recorded, for the reason every addition to the
|
|
host's vocabulary is one: the set is what a person reads to learn what a mesh can have, and an entry
|
|
nobody argued for is an entry nobody can explain.
|
|
|
|
## The set
|
|
|
|
**The set is derived, and only the mesh's half is written here.** Revision, 2026-09-26
|
|
([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md), superseding
|
|
[ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)): a module
|
|
declares its own seats with their protocols, so the seats a mesh has are the mesh's own **plus
|
|
every registered module's**. The set is still closed — a seat named nowhere is refused — but it is
|
|
computed from the catalogue rather than maintained by hand, which is the property 0110 actually
|
|
needed and the table could not keep.
|
|
|
|
**And the mesh's own half is data, named for its scope.** Revision, 2026-09-27, reconciling two
|
|
records made in parallel: [ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
|
|
names a system seat for the scope it is held at — `mesh-*` for one per mesh, `node-*` for one per
|
|
machine — and [ADR 0122](../../02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md) moves
|
|
the set out of compiled code into a table the controller owns, so a rename is one write rather than a
|
|
rebuild of everything that names one.
|
|
|
|
So the set has two halves and neither is written out here: the mesh's own, which the controller holds
|
|
as rows, and every registered module's, which is computed from the catalogue. What this document keeps
|
|
is what a seat *is* — the rest would be a third copy, stale the first time somebody renamed one, which
|
|
is the fault ADR 0122 exists about.
|
|
|
|
|
|
**Every seat below is named `mesh-*`, and the prefix is the reservation rule**: a module declaring
|
|
any `mesh-*` name is refused at registration, so there is no reserved-names list to drift. Ten of
|
|
these are renamed to restore [ADR 0079](../../02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md)'s
|
|
convention, which later seats departed from.
|
|
|
|
| seat | was | scope | delivers | typically held by |
|
|
|---|---|---|---|
|
|
| `mesh-controller` | — | mesh | — | the controller |
|
|
| `mesh-store` | — | mesh | — | the store the mesh's own records live in |
|
|
| `mesh-broker` | — | mesh | `mesh-bus` | the broker carrying the mesh's own bus |
|
|
| `mesh-vault` | — | mesh | `secret`, reserved | the vault |
|
|
| `mesh-artifact-store` | `the-artifact-store` | mesh | `artifact-store` | the artifact registry |
|
|
| `mesh-catalog` | `the-catalogue` | mesh | — | the catalogue |
|
|
| `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge |
|
|
| `mesh-git` | `git` | mesh | `git` | the forge |
|
|
| `mesh-build-machine` | `the-build-machine` | node | — | a builder |
|
|
| `mesh-dns-port` | `the-dns-port` | node | — | the local resolver |
|
|
| `mesh-intrusion-prevention` | `the-intrusion-prevention` | node | — | an intrusion-prevention service |
|
|
| `mesh-packet-filter` | `the-packet-filter` | node | — | the packet filter |
|
|
| `mesh-private-network` | `the-private-network` | node | — | the private network the mesh runs over |
|
|
| `mesh-resolver-configuration` | `the-resolver-configuration` | node | — | whichever of the alternative resolver configurations is chosen |
|
|
| `mesh-showcase` | `the-showcase` | node | — | the showcase module |
|
|
|
|
The controller holds **the mesh's own** entries in code, and a test asserts their size and that
|
|
every one names the record that made it a seat. A module's seats are not here and never will be —
|
|
they are read from the catalogue. **This table and
|
|
[ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) govern, and code that
|
|
disagrees is what is wrong.** The implementation in progress predates several
|
|
things here: seats held by assignments rather than claimed by definitions, the `mesh-vault` seat and
|
|
its reservation, and the foundation's seats delivering nothing. It is brought to this table before it
|
|
merges.
|
|
|
|
## The foundation's seats
|
|
|
|
`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. **They route no consumer.** The
|
|
store and broker modules may run on other nodes too. A database or `amqp` consumer is served by
|
|
co-location, from whichever runs on its own node, the seat's holder included
|
|
([23 — Choosing a provider](23-choosing-a-provider.md)). A requirement cannot name one of them,
|
|
because they deliver nothing.
|
|
|
|
## A seat that delivers a provision
|
|
|
|
**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 decision. 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 the seat, and then its holder answers.** Naming the seat asks for *the
|
|
mesh's* one, so the holder answers **even when another provider runs on the consumer's own machine**,
|
|
and nobody is asked anything. With the seat unheld, the requirement is refused, naming the seat. A
|
|
second provider can run beside the holder and harm nothing. A forge assignment holds
|
|
`npm-package-registry`, and an npm proxy may provide the same provision on another machine. A builder
|
|
that names the seat is still served by the forge, without anybody pinning it.
|
|
|
|
**A requirement that names no seat resolves as any other**: a pin, the provider on the consumer's own
|
|
machine, the only provider. If several remain and none is local, a person chooses when the module is
|
|
assigned. The candidates are listed with the seat's holder suggested first, and the answer is recorded
|
|
as the assignment's pin ([27](27-a-module-requires-the-mesh-resolves.md)). Nothing is guessed, and
|
|
nothing changes silently because a second provider happened to appear nearby.
|
|
|
|
**Moving the role is changing which assignment holds the seat.** No definition changes and nothing is
|
|
unassigned: the forge keeps running, and keeps holding `git`, when its npm role moves. A module can
|
|
take the role only if its definition says it can hold the seat.
|
|
|
|
**The vault's provision is reserved.** Only an assignment holding `mesh-vault` may provide `secret` at
|
|
all: a definition providing it that cannot hold the seat is refused, an assignment providing it without
|
|
holding the seat is refused, and a `secret` requirement always names the seat, because there is no
|
|
other provider. A second provider of secrets would be a second place secrets live, which is what the
|
|
vault being one per mesh exists to prevent.
|
|
|
|
**What a consumer receives is what it required**, the same as for any provision: where the provider
|
|
answers, what it serves, and a credential. A consumer never reads the seat directly. The one exception
|
|
is the controller itself, which reaches the store and the broker through a narrow seat placeholder,
|
|
because it made them before any module existed and cannot be their consumer. One foundation module
|
|
also reads it today, to find its own server's port. [27](27-a-module-requires-the-mesh-resolves.md)
|
|
moves that to a host port requirement.
|
|
|
|
## A seat that delivers nothing
|
|
|
|
**A module's declared seat may promise nothing too, and that is a marker seat.** Correction of fact,
|
|
2026-09-27: [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)'s "a declared seat
|
|
carries a protocol" governs what a *holder* must satisfy, not that every seat offers something. A
|
|
marker seat's protocol is satisfied by holding it, which is the whole of what a marker says. Refusing
|
|
an empty one would refuse most of the node-scoped set, the showcase module's own seat included.
|
|
Nothing reaches that state by accident: an unknown manifest field is refused outright, so an empty
|
|
protocol was written as one. Checked by a registration test accepting a node seat with no protocol
|
|
and by the showcase manifest, which declares one.
|
|
|
|
Most node seats deliver nothing. They say which module is this machine's packet filter, or which of
|
|
two alternative resolver configurations it runs, and a second holder is refused. That is the whole of
|
|
their job, and it is a real one: it is the mesh saying what a machine is, in words a person can read.
|
|
|
|
## The overview
|
|
|
|
The controller lists every seat in the set with its scope, what it delivers, and its holder as a node
|
|
and a module. A seat nobody holds is listed as unheld. That is an answer, "this mesh has no forge",
|
|
and not a fault.
|
|
|
|
Holdings are derived from assignments whenever they are asked for, never stored. The list is always
|
|
what the mesh is running, because it is computed from the same thing that decides what the mesh runs.
|
|
|
|
## The git seat, and where a build comes from
|
|
|
|
A module is built from a repository, a path and a ref. The repository is one of two things, and the
|
|
mesh records which:
|
|
|
|
| form | means | recorded as |
|
|
|---|---|---|
|
|
| on the `git` seat | a repository on the forge that holds the seat | its path on the forge, and the seat |
|
|
| external | a repository anywhere else, a public forge for instance | its URL, exactly as given |
|
|
|
|
For a repository on the seat, the controller composes the clone URL at the moment of building, from
|
|
where the holder runs and the scheme and port it serves for `git`. The recorded source never contains
|
|
an address, so moving the forge changes nothing that was recorded. The build machine is not told the
|
|
difference: it receives a URL either way.
|
|
|
|
With the seat unheld, a build from the seat is refused and says why. External builds carry on.
|
|
|
|
**Not yet designed:** a credential for cloning a private repository. The mesh's own repositories are
|
|
public. The natural place for a clone credential is a `secret` from the vault, and that is a decision
|
|
still to take.
|
|
|
|
## How it is checked
|
|
|
|
The rules here are [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)'s —
|
|
which supersedes [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)
|
|
and keeps every rule below except how the set is formed — and
|
|
[ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md)'s. Each is
|
|
checked as their tables say:
|
|
|
|
| Rule | Checked by |
|
|
|---|---|
|
|
| The set is closed, and every mesh entry names its decision | 0118: a unit test on the mesh's own entries. **The refusal of an unknown seat is at registration, not in the parser** — correction of fact, 2026-09-27: a module may hold a seat *another* module declares, which is the point of naming the seat and not its provider, so whether a claimed name exists is a fact about the whole catalogue and a manifest in isolation cannot be judged on it. Registration tests cover an invented name and a name another module declares; the parser still refuses a claim on the mesh's own `mesh-*`/`node-*` namespace and a scope that disagrees with a declaration in the same manifest. |
|
|
| The set is derived, and enumerating it is a query | 0118: the overview lists the mesh's own plus every registered module's, asserted against a fixture mesh. |
|
|
| `mesh-*` is the mesh's, and a module may not declare one | 0118: a registration test refusing a manifest that declares any `mesh-*` seat, naming the prefix. |
|
|
| Two modules cannot declare the same seat | 0118: a registration test; the second is refused and the first untouched. |
|
|
| A holder satisfies the seat's protocol | 0118: a claim whose module does not serve what the seat declares is refused at assignment. |
|
|
| A seat is held by one assignment, and only by one whose module can hold it | 0118: resolution tests for a second holder and for a seat the definition does not name. 0131: `CanHold` is the one judgement, shared by registration and the handover, and its test follows the store's row. |
|
|
| A holder on record settles the seat; another eligible assignment is silent, not refused | 0131: resolution tests with a recorded holder on the same machine, on another machine, and under a seat's former name; without a record, the old rule's tests still pass unchanged. |
|
|
| A handover replaces the holder as one write, needs an assignment to point at, and goes with it | 0131: store tests — a second handover leaves one row; a handover to a module not assigned where named is refused; unassigning the holder removes the row. |
|
|
| A requirement naming a seat is answered by its holder; a foundation seat cannot be named | 0118: resolution tests with a second provider on the consumer's node, with the seat unheld, and naming `mesh-store`. |
|
|
| Several providers and none local is a person's choice | 0118: an assignment test listing candidates with the seat's holder first and recording the pin. |
|
|
| `secret` is reserved | 0118: the parser and resolution refusals for another provider and a pin. |
|
|
| Holdings are derived, and the overview lists every seat | 0118: the `seats` command test, including an unheld seat. |
|
|
| A build source on the seat records no address; an unheld seat refuses only self-hosted builds | 0111's tests. |
|