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
+3 -15
View File
@@ -2,11 +2,10 @@
layer: to-be
status: designed
code: []
updated: 2026-09-25
updated: 2026-09-20
decisions:
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
- 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
---
# 23 — Choosing a provider
@@ -51,20 +50,9 @@ provider on a different node. That coupling is exactly what may not be guessed,
names the provider. Naming it is also what makes a later move safe — the mesh knows the binding is
to that provider and not to whichever one is nearest.
**Some provisions have one provider for the whole mesh, and a seat names it.** Where a seat delivers
the provision, its holder answers for it, **and co-location does not apply**: a second provider on the
consumer's own machine does not take over for that consumer. That is not picking: the choice was made
once, mesh-wide, by assigning the holder, rather than once per consumer by naming it
([ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md),
[26 — The seats](26-the-seats.md)). A named provider still wins over the seat, because a consumer
coupled to particular contents has said so, except for `secret`, which only the vault may provide.
Only provisions the design makes one-per-mesh are delivered by a seat: the broker, the artifact store,
a package registry, git and the vault. A database is not. Node-local stores, served by co-location,
are the rule above.
**Ambiguity is refused, never resolved by picking.** If several providers of a kind exist, none is
named, none is co-located, and no seat delivers it, the requirement is unsatisfiable and is refused
with the candidates shown — the same stance
named, and none is co-located, the requirement is unsatisfiable and is refused with the candidates
shown — the same stance
[ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) took
against a confidently-wrong match, applied to the instance rather than the dialect. A wrong answer
delivered quietly costs more than a refusal.
+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.
@@ -96,9 +96,17 @@ per consumer, named for that consumer:
Every other shared secret takes the same path:
- a module's own secret;
- every broker account's password, where the broker's own provisioner creates the account;
- an enrolment token;
- a secret operator value, which the operator delivers to the vault.
- every broker account's password on the mesh's bus, where the broker's own provisioner creates the
account;
- an enrolment token, which the operator receives and the controller can only verify;
- a secret operator value, which the operator delivers to the vault;
- a secret a backend issues itself, such as a forge's API token, which the module that received it
delivers to the vault.
**Parties that are not modules take the same path too.** The controller's own store login and bus
account, and each node agent's bus account, have no definition to require them, because the controller
and a node agent are the mesh itself. The controller asks the vault on its own behalf or a node's, and
the answer is made, sealed and carried exactly as for a module.
**A provider makes resources and data.** Beyond secrets, a provider's adapter may answer with its
contract's non-secret fields: an analytics site id, a registered public name. The mesh carries them
@@ -116,7 +124,7 @@ lost is a named volume, not a directory ([ADR 0030](../../02-DECISIONS/0030-data
*Where* a directory is on the machine is the assignment's:
- **a node's default layout**, a root per node with one directory per instance beneath it, used when
- **a node's default layout**, a root per node with one directory per assignment beneath it, used when
the assignment says nothing;
- **a placement**, where the assignment puts one directory elsewhere: on a second disk, or where an
adopted machine's data already is ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
@@ -175,35 +183,39 @@ module uses the seat placeholder.
decided. The one exception 0086 allows is a declared env-file with its reason; a secret field as a
value in a container's environment is refused when the definition is parsed, with no exception.
## An instance
## An assignment
An assignment has an identity: an **instance name**, which defaults to the module's name. Everything
keyed by the module's name today is keyed by the instance: directories, containers, the login it
presents, its broker account, the seats it holds, its settings and its identity as a provider.
**A module is assigned at most once to a node**, and that pair is the assignment's identity
([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md)). Its directories,
containers, login, broker account and settings are keyed by it, as today, and a login still fits the
tightest backend ([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)).
So a module may run twice on one node, under two instance names. What must stay singular stays so:
by a seat, or by an operator value colliding, as with a public name.
**A module may run on many nodes, and one assignment may hold a seat**
([ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)). The definition
says which seats the module can hold; the assignment says which it does. So the store module can run
on every node, one of those assignments holds `mesh-store`, and moving that role changes an
assignment, not a definition.
**A login still has to fit the tightest backend**, which is twenty characters today
([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)). A node name and
an instance name will not fit in full. The instance therefore gets a short form alongside the module's
slug, under the same rules as a slug. This has to be settled before a second instance is allowed.
What must stay singular stays so: by a seat, or by an operator value colliding, as with a public name.
## Genesis
**Genesis delivers, and the vault adopts.** The vault cannot run first: it is built on the shared
runtime base, which the installation makes only after the store, the broker and the controller exist
([21 — The installation in full](21-the-installation-in-full.md)), and it learns what to answer from
the controller over the bus. So genesis generates the foundation's first shared secrets itself:
- the store's superuser;
- the broker's admin, in the hashed form the broker needs;
- the bus accounts of the temporary controller and of the vault;
- the first enrolment token.
the controller over the bus. So the vault is installed **as soon as that base exists**, before any other
module built on it, and genesis generates what is needed until then:
- the store's superuser, and the broker's admin in the hashed form the broker needs;
- the bus accounts of the temporary and permanent controller, the control-node's agent, the builder,
the broker's own provisioner and the vault;
- the controller's store login, and the first enrolment token.
It seals them to the operator key as today ([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md)).
When the vault is installed, genesis **delivers them to it**, through the same path an operator's value
takes. From then on the vault holds, audits and rotates them. It can make their replacements, unlike
an operator's external key.
Until the broker's provisioner runs, genesis creates those bus accounts with the broker's admin, as the
controller does today; the provisioner adopts them when it starts. Genesis seals everything to the
operator key as today ([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md)), and when the
vault is installed it **delivers the values to it, recorded as the mesh's own**. That distinction keeps
them rotatable: an operator's value is never replaced, and these are, because the vault can make their
replacements.
That is the one time anything but the vault generates a shared secret, and it ends by handing them
over. It is also the answer to the objection ADR 0085 had to the vault being the only maker: the
@@ -235,10 +247,16 @@ only when it first initialises is marked applied, because a restart would change
recipient that reads at start has restarted and passed its health check, where its definition
declares one. Delivered and working are shown as different things.
**The remaining window is stated.** An applier whose provisioner stops after applying and before
confirming leaves the recipients that read at start locked out: the old value no longer works, and
they have not been sent the new one. A provisioner is supervised and restarted when it exits, so the
window lasts until that restart. The rotation shows as waiting on that applier throughout, never as done.
**Open: keeping readers from being locked out.** Steps 2 and 3 leave a reader locked out when its
machine is offline after an applier applied, when the secret is a bus account whose owner loses the bus
it would hear the new value on, or when a restarted provisioner can no longer check the old value.
[ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md) records the two answers, overlapping
old and new credentials or re-confirming with safeguards, and one is chosen before it is accepted.
Neither changes a consumer module.
A secret some service reads only when it first initialises cannot be rotated by restarting it. It is
applied by a provisioner, or, where none exists, marked not rotatable by the mesh, and a rotation is
refused rather than reported done.
## Refusing
@@ -258,10 +276,11 @@ as waiting, and nothing is delivered until the answer arrives.
| mechanism | becomes |
|---|---|
| provisions read through bindings | a module requirement; its answer is the contract's fields |
| settings on an assignment ([ADR 0046](../../02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md)) | operator requirements, addressed to an instance |
| settings on an assignment ([ADR 0046](../../02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md)) | operator requirements on an assignment |
| a port the mesh assigns | a host requirement |
| machine facts and machine placeholders | host requirements |
| every secret the controller mints: provider credentials, own secrets, broker passwords, root secrets | a secret the vault makes ([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)) |
| every secret the controller mints: provider credentials, own secrets, broker passwords, enrolment tokens | a secret the vault makes ([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)) |
| root secrets genesis mints and keeps apart | made by genesis once, then delivered to the vault, which holds and rotates them |
| a separate command issuing a broker account | a requirement resolved on assignment |
| `restart-on` naming a secret's file | a restart the host derives |
| paths in resources, mounts, bindings, secrets and received files | host directory requirements, placed by the assignment |
@@ -287,11 +306,11 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r
is fixed first. *Ends when* nothing outside the vault generates a shared secret after genesis, a lab
consumer of analytics receives its site id, and a database credential rotates applier-first, with
the consumer restarted by derivation and the rotation confirmed.
3. **Definitions move.** Every catalogue definition is rewritten, adopted and running assignments
placed where their data already is. *Ends when* the list of definitions using an old form is
empty, and the old forms are removed.
4. **Instances.** The instance name and its short form, and everything keyed by it. *Ends when* one
module runs twice on one lab machine with two public names.
3. **Definitions move, and seats move to assignments.** Every catalogue definition is rewritten, adopted
and running assignments placed where their data already is, and each claim becomes a seat the
module can hold, held by the assignment that holds it today. *Ends when* the list of definitions
using an old form is empty, the old forms are removed, and the store module runs on two lab
machines with one holding `mesh-store`.
## How it is checked
@@ -305,8 +324,9 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r
| A host requirement is answered on its own node | A resolution test placing one elsewhere: refused. |
| An operator value needs no provider module | A resolution test: a requirement with a default resolves with no module assigned anywhere. |
| A secret field reaches a process as a file | The parser refuses a secret field as a container environment value, and accepts it in a file or a declared env-file with its reason. |
| A public name already held is refused | A resolution test: a second instance asking for a public name the first holds is refused, naming the first. |
| Everything keyed by a module is keyed by its instance | A resolution test: two instances of one module on one node get two directories, two containers, two logins and separate settings. |
| A public name already held is refused | A resolution test: a second assignment asking for a public name another holds is refused, naming the holder. |
| A module is assigned at most once to a node | A resolution test: assigning a module to a node that already runs it is refused. |
| A seat is held by an assignment, not a module | A resolution test: the store module on two nodes, one holding `mesh-store`; a second assignment asking to hold it is refused. |
| A consumer waits for its provider | A resolution test with a provider that has not answered: shown as waiting, and nothing delivered. |
| Only the vault generates a shared secret after genesis | A controller test: no code path generates one. An installer test: genesis generates exactly the foundation's first secrets and delivers them to the vault. |
| Only the vault provides `secret` | The parser refuses another provider of it, and resolution refuses a pin on a `secret` requirement. |
@@ -318,11 +338,8 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r
## Not settled here
- The exact spelling of the one form. It must name a requirement and a field and nothing else.
- The layout a node's default root uses beneath it, beyond one directory per instance.
- The layout a node's default root uses beneath it, beyond one directory per assignment.
- Whether a module provider's answer can change without the provider being asked, for example a
provider moving. The rule so far is that it cannot, and moving is re-resolving.
- **Which seats a module holds.** Today a claim is part of the definition, so moving a seat is a
definition change ([26 — The seats](26-the-seats.md)). By this document's own logic it belongs to
the assignment: a definition says which seats a module *can* hold, and the assignment says which it
*does*. That changes [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md),
and is its own decision.
- **How rotation keeps a recipient from being locked out.** Under review: see
[ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), rotation.