Seats held by assignments, one assignment per module per node, and 0113's bottom of the stack

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.
This commit is contained in:
jochen
2026-09-25 23:47:26 +02:00
parent 1b5f2c2c1a
commit 4a1b218706
9 changed files with 346 additions and 296 deletions
+56 -54
View File
@@ -1,6 +1,6 @@
---
layer: to-be
status: in-progress
status: proposed
code:
- mesh-controller internal/catalogue/seats.go
- mesh-controller internal/catalogue/resolve.go
@@ -17,8 +17,8 @@ decisions:
# 26 — The seats
**What a mesh can have one of, and who fills each.** A seat is a named role at a scope, taken by a
module assignment. The mesh defines which seats exist. Occupying one may deliver a provision, and the
**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
@@ -27,28 +27,31 @@ A seat has four properties, fixed by the mesh rather than by any module:
| property | is |
|---|---|
| name | what a manifest claims, and what a person reads in the list |
| 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 module assignment holds a seat by claiming it.** The claim is the manifest's `claims`, and it is
satisfied by assigning the module somewhere. The seat is not a second record beside the assignment.
It points at the assignment, and 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.
**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 set is closed.** A claim naming a seat the mesh does not define is refused, and so is a claim 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 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.
**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
| seat | scope | delivers | typically held by |
|---|---|---|---|
| `mesh-controller` | mesh | — | the controller |
| `mesh-store` | mesh | — | the foundation's store |
| `mesh-broker` | mesh | `amqp` | the broker |
| `mesh-store` | mesh | — | the store the mesh's own records live in |
| `mesh-broker` | mesh | — | the broker carrying the mesh's own bus |
| `mesh-vault` | mesh | `secret`, reserved | the vault |
| `the-artifact-store` | mesh | `artifact-store` | the artifact registry |
| `the-catalogue` | mesh | — | the catalogue |
@@ -64,27 +67,24 @@ argued for is an entry nobody can explain.
The controller holds this set in code, and a test asserts both its size and that every entry names
the record that made it a seat. **This table and [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)
govern, and code that disagrees is what is wrong.** The implementation in progress predates three
things here: the `mesh-vault` seat and its reservation, and the rule that `mesh-store` delivers
nothing. It is brought to this table before it merges.
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 seat that delivers a provision
A seat that delivers a provision may only be held by a module that provides it, at the seat's scope.
A mesh seat delivers a mesh-scoped provision.
**A seat delivers a provision only where the mesh has one answer for everyone.** The broker, the
artifact store, the npm registry, git and the vault are each one per mesh by decision. The store is
not: nodes run their own stores and a consumer uses the one on its machine
([23 — Choosing a provider](23-choosing-a-provider.md)), and the foundation's store is the controller's
own memory, provider to nobody. So `mesh-store` guards that the foundation's store is singular, and
routes nobody.
**The vault's provision is reserved.** Only the holder of `mesh-vault` may provide `secret` at all: a
module providing it without the seat is refused, and a pin cannot choose another provider, because
there is none. A second provider of secrets would be a second place secrets live, which is what the
vault being one per mesh exists to prevent. Every other delivered provision may have second
providers, which a pin can choose.
**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.
**Its holder answers for that provision.** A requirement for it resolves, in order, to:
@@ -96,21 +96,23 @@ providers, which a pin can choose.
Co-location, which answers first for every other provision, does not apply here: a seat says which
one is the mesh's, and co-location answering first would let any second provider on a consumer's
machine take over for that consumer, silently. So a second provider can run beside the holder and
harm nothing. The forge holds
`npm-package-registry`. An npm proxy may provide the same provision on another machine, and a module
requiring an npm registry is still served by the forge, without anybody pinning it.
harm nothing. A forge assignment holds `npm-package-registry`. An npm proxy may provide the same
provision on another machine, and a module requiring an npm registry is still served by the forge,
without anybody pinning it.
**Moving the role is changing which module claims the seat, and today that is a definition change.**
A claim is part of a module's definition, so the proxy's definition must claim the seat and the
forge's must stop. The forge cannot simply be unassigned, because it holds `git` as well. Every
consumer follows once the claim moves. Making *which* seats a module holds the assignment's choice,
with the definition saying only which seats it *can* hold, is the consistent answer, and
[27 — A module requires, the mesh resolves](27-a-module-requires-the-mesh-resolves.md) lists it as
not yet settled.
**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.
**What a consumer receives is a grant**, 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,
**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 pin cannot choose another provider, because there is none. 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.
@@ -123,9 +125,9 @@ their job, and it is a real one: it is the mesh saying what a machine is, in wor
## The overview
The controller lists every seat in the set with its scope, what it delivers, and each 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.
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.
@@ -140,13 +142,13 @@ mesh records which:
| 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.
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 the `git` provision's grant, and that is a
decision still to take.
public. The natural place for a clone credential is a `secret` from the vault, and that is a decision
still to take.