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
+4 -11
View File
@@ -44,22 +44,15 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
## How modules relate to the mesh
- **seat** — a named role at a scope (node / site / mesh), held by a module assignment, from a
**closed set** the mesh defines: a claim naming a seat outside the set is refused. A seat may
**deliver a provision**, and its holder is then the mesh's answer for it when several modules
provide it ([ADR 0110](../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)).
The set, with who holds each seat, is the overview of what a mesh has
([26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md)). A seat has a **capacity**: a
capacity-1 seat is exclusive (one holder); a higher-capacity seat is a **bench** (several holders
coexist).
- **seat** — a named position at a scope (node / site / mesh) with a **capacity**. A capacity-1 seat
is exclusive (one holder); a higher-capacity seat is a **bench** (several holders coexist).
- **claim** — a module taking a spot on a seat. `claims: [{name, scope}]` in a manifest. A
mesh-scoped exclusive claim is how the mesh says "there is one of me". A foundation seat is
named after the server it guards: the `mesh-controller`, `postgres` and `lavinmq` modules claim
the `mesh-controller`, `mesh-store` and `mesh-broker` seats ([ADR 0079](../02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md)).
- **provision** — a service one module `provides` and others `require`; the mesh resolves a provider
and wires the two with an endpoint and a credential. A provision is a service you offer, a seat
is a role you occupy, and the two meet where a seat delivers a provision: occupying the seat is
what makes a module *the* provider of it.
and wires the two with an endpoint and a credential. This is separate from seats: a provision is
a service you offer, a seat is a slot you occupy.
## How this page is kept
@@ -1,20 +1,20 @@
---
topic: what runs on it
status: accepted
status: proposed
date: 2026-09-25
deciders: jochen
reconstructed: false
extends: 0009-modules-and-the-graph.md
---
# 110. A seat is a module assignment from a closed set, and it may deliver a provision
# 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 three things are now true of it that no record says.
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.
@@ -24,100 +24,102 @@ The names in use were each invented by the module that claims them: `the-showcas
**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.
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.
**Some seats are the mesh's one of something that others consume, and nothing uses that fact.**
Of the thirteen claims, four are held by a module that provides something consumers require:
`mesh-store` (`postgres-database`, eleven consumers), `mesh-broker` (`amqp`), `the-artifact-store`
(`artifact-store`) and `the-dns-port`. The rest deliver nothing to anybody, and are still
meaningful: they say which module is this mesh's packet filter, or resolver configuration.
Meanwhile 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.
**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, and keep provisions as the only thing that delivers.**
Rejected. The overview stays unanswerable, and a second provider of anything costs a pin per
consumer node — the decision "gitea is our npm registry" made again on every machine.
**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.
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 a module assignment from a closed set, and occupying it may deliver a provision.**
**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 occupying it
delivers (if anything), and the decision that made it a seat. A claim naming a seat outside the set
is refused, and so is a claim at a scope other than the seat's. 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.
**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 seat is held by a module assignment.** 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.
**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.
**A seat may deliver a provision, and then its holder answers for it.** A seat that delivers a
provision can only be held by a module that provides it, at the seat's scope, and a claim that does
not is refused. A requirement for that provision resolves, in order, to:
**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.
1. a provider the consumer's node was **pinned** to — a consumer coupled to one provider's
contents, as [to-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md) already allows;
2. **the holder of the seat** that delivers it, **even when another provider runs on the consumer's
own node**;
**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:
1. a provider the consumer's node was **pinned** to, a consumer coupled to one provider's contents;
2. **the holder of the seat**, **even when another provider runs on the consumer's own node**;
3. otherwise refused, naming the unheld seat.
**Co-location does not apply to a provision a seat delivers.** For every other provision, a provider
on the consumer's own node answers first, then the only provider, then refusal
([to-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md)). A seat exists to say *which one is the
**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](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md)
names for the vault.
names for the vault. This keeps [ADR 0009](0009-modules-and-the-graph.md)'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.
This keeps [ADR 0009](0009-modules-and-the-graph.md)'s rule that a requirement with several answers is
never guessed. The seat is not a guess. It is the choice made once, mesh-wide, by assigning the holder,
instead of once per consumer by pinning. A second provider may run beside the holder, and whatever
requires the provision still resolves to the holder without anybody naming it.
**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.
**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 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 delivers a provision only where the mesh has one answer for everyone.** That is a design
decision about the provision, not about the seat. The broker, 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 store
is not: [to-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md) has each node running its own
stores, with a consumer served by the one on its own machine, and the foundation's store is the
controller's own memory, provider to nobody ([to-be 21](../03-DESIGN/01-to-be/21-the-installation-in-full.md)).
So `mesh-store` guards that the foundation's store is singular, and delivers nothing. Were it to
deliver, every database consumer on every node would be sent to the control-node's store.
**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](0085-a-secret-is-a-provision.md), as amended), and a second `secret` provider is exactly
that, whether a pin chose it or not.
**A seat may reserve its provision.** Where a second provider would break a rule the provision exists
for, only the seat's holder may provide it at all: the parser refuses anyone else, and a pin cannot
choose anyone else. `secret` is the one reserved provision. The vault is one per mesh because a second
one *"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. Every other delivered
provision may have second providers, which a pin can choose.
**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 module is refused by
it:
**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 | held today by | made a seat by |
| 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 | `amqp` | `lavinmq` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
| `mesh-vault` | mesh | `secret`, reserved | nothing yet: `mesh-vault` claims it | this record, for [issue 106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.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) |
@@ -131,57 +133,62 @@ it:
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.
gitea claims it. verdaccio provides the same provision and claims nothing, so it is the second
provider this record exists to make harmless.
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. A second vault would have answered requirements silently, and any consumer on
its machine would have been served by it through co-location. The seat is named after its server, by
the 0079 convention, and the vault module claims it.
`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. Whether the seat should say it delivers it is left for when a second resolver makes the
question real.
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): "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.
## 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.
This is the pattern the host's vocabulary test already follows.
- Manifest validation refuses an unknown seat, a seat claimed at the wrong scope, and a
provision-delivering seat claimed by a module that does not provide the provision. The three
refusals name the seat and the set.
- Resolution prefers the seat's holder among several providers, after a pin. A provider record
gains the module it came from, because two modules on one node could otherwise not be told apart
as holder and non-holder.
- A `seats` command lists the set with each seat's holders, derived from assignments.
- gitea claims `npm-package-registry`. The catalogue's `package-registry` becomes
`npm-package-registry` throughout, per [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md).
- 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 `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. That is the point, and it costs one record per seat.
- **Not changed:** `${seat:<seat>:<port>}` stays as it is. It exists so the controller can reach a
foundation it made before any module existed, and it cannot be a consumer. A module that needs
something from a seat's holder requires the provision the seat delivers, and receives it the way
any provision is received: through a grant.
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 claim outside the set is refused | Manifest-validation tests for an unknown seat, the wrong scope, and a delivering seat whose claimant does not provide. |
| Every module in use claims a seat in the set | A controller test parses every catalogue manifest and fails on any refused claim. The lab's beds read the same manifests ([ADR 0089](0089-a-bed-reads-the-catalogue-it-proves.md)). |
| 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 machine, where the holder still answers; the seat unheld with two providers, and with **one** provider, both refused naming the seat. |
| A seat delivers only a one-per-mesh provision | A controller unit test: `mesh-store` delivers nothing, so a database consumer is still served by co-location, and `mesh-broker` delivers `amqp`. |
| A reserved provision has no other provider | The parser refuses a module providing `secret` without claiming `mesh-vault`, and resolution refuses a pin on a `secret` requirement. |
| 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. |
| 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](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
- [to-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md): pins, co-location and refusal
- [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)
@@ -1,6 +1,6 @@
---
topic: building it
status: accepted
status: proposed
date: 2026-09-25
deciders: jochen
reconstructed: false
@@ -46,7 +46,7 @@ one case it exists for: after the forge moves, old URLs no longer match anything
**The mesh has a `git` seat.** It is mesh-scoped and delivers the `git` provision, per
[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md). Its holder provides `git`,
serving how a repository on it is cloned: the scheme and the port. gitea claims it.
serving how a repository on it is cloned: the scheme and the port. A gitea assignment holds it.
**A source is on the git seat, or it is external, and the mesh records which.**
@@ -72,7 +72,7 @@ the controller's job, because only the controller knows where the seat's holder
- `build`, `build --behind` and `build --dry-run` resolve a seat source before asking a builder. The
recorded source keeps the seat form; the build log keeps the URL that was actually cloned, because
that is what happened.
- gitea claims `git` and provides it, serving HTTP clone on its web port.
- gitea can hold `git` and provides it, serving HTTP clone on its web port; the forge's assignment holds the seat.
- **Not decided: credentials for private repositories.** The mesh's own repositories are public, and
clone without one. A private repository still works only if the build machine's own git
configuration authenticates, exactly as before. Delivering a clone credential through the `git`
@@ -43,7 +43,7 @@ concept that joins them.
**1. Keep host paths in definitions, and check that every copy agrees.** Rejected. It checks the
agreement of something that should not be there. A definition still could not follow its data to
another disk, be adopted onto a machine whose data is already somewhere, or run twice on one node.
another disk, or be adopted onto a machine whose data is already somewhere.
**2. Keep the separate mechanisms, and add directories as a seventh.** Rejected. It fixes paths and
keeps the pattern that produced them: each mechanism is resolved, validated and refused differently,
@@ -105,39 +105,38 @@ directory. The mesh mounts the assignment's location there. No host path is ever
reads, and the mesh's own files (answers, contributions) name nothing by host path, so a provider needs
no mount at a machine-identical path.
**An assignment has an identity of its own: an instance name**, defaulting to the module's name.
Everything keyed by the module's name today is keyed by the instance instead: directories, container
names, the login a consumer presents, broker accounts, a claim's holder, the settings an assignment
carries, and a provider's identity. So **one module may be assigned to one node more than once.** What
must stay singular stays so by a claim, or by an operator value colliding: a public name already taken
is refused like any other singular thing.
**A module is assigned at most once to a node.** An assignment is a module on a node, and that pair is
its identity: its directories, containers, login, broker account and settings are keyed by it, as
they are today. A module may run on many nodes, and one of those assignments may hold a seat
([ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)). Running the same module twice
on one machine is not supported. The cases that seemed to need it, such as two stores of one engine or
two stages of one application, are different modules, or the same module on different machines. The
line is drawn because every identity in the mesh is already a module on a node, and a second
instance would have to rename all of them.
**Genesis is not an exception.** It raises the vault first and asks it for the foundation's secrets,
so the foundation's requirements are answered the same way as everything else
([ADR 0113](0113-the-vault-makes-every-secret.md)).
**What must stay singular stays so** by a seat, or by an operator value colliding: a public name
already held by another assignment is refused like any other singular thing.
**The foundation's first secrets are delivered, then adopted.** Genesis generates them before the vault
can run and hands them to the vault once it is installed, and from then on they are answered the same
way as every other secret ([ADR 0113](0113-the-vault-makes-every-secret.md)).
## What this changes in earlier records
On acceptance, each of these is amended by a record of its own, not edited:
- [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md): settings become
operator requirements, addressed to an instance rather than to a module on a node.
operator requirements on an assignment.
- [ADR 0038](0038-the-mesh-assigns-the-port.md): a port becomes a host requirement. What 0038 decided
is unchanged; it is the first case of this rule.
- [ADR 0051](0051-shared-data-is-the-operators.md): an access keeps its shape and its semantics; its
path moves from the definition to the assignment.
- [ADR 0091](0091-a-mount-is-declared-three-ways.md): a mount's host side is a resolved requirement,
checked as resolved rather than as a path the definition declares.
- [ADR 0084](0084-which-provider-serves-a-consumer.md): a provider is a (node, instance) pair, not a
(node, module) pair, so a consumer can name one of two instances on one node.
- [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md): a login is built from the
instance, which gets a short form under the same rules as a slug, so it still fits the tightest
backend.
- [To-be 26](../03-DESIGN/01-to-be/26-the-seats.md): a seat's holder is an instance.
- The [glossary](../00-META/glossary.md): *provision* widens from "a service one module provides" to a
requirement answered by any of the four providers, and *instance* is added. Neither lands while this
record is only proposed, because the glossary is the authority on the words in use, not on words
under review.
requirement answered by any of the four providers, and *requirement* and *contract* are added. None
of it lands while this record is only proposed, because the glossary is the authority on the words
in use, not on words under review.
## Consequences
@@ -148,13 +147,13 @@ On acceptance, each of these is amended by a record of its own, not edited:
- The controller resolves every requirement at assignment and refuses unresolved ones. The host
answers directories and ports. The settings, placeholders, facts and bindings that exist today
are retired as separate mechanisms, once nothing uses them.
- Identity moves from the module to the instance, which touches logins, broker accounts, settings,
provider selection and every resource name. A login already has a 20-character limit
([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md)), which a node and an instance
name will strain. The design must answer that before a second instance is possible.
- **What got harder:** a definition no longer says where a module's data is on a machine, or what a
setting's value is. The assignment does, and `plan` shows it. That is the point, and it is also a
real loss of at-a-glance legibility, which the overview has to give back.
- Identity stays a module on a node. Nothing is renamed, and a login still fits the tightest backend
as [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md) arranges.
- **What got harder:** one module cannot run twice on one machine; a second stage or a second store
of one engine is a different module or a different machine. And a definition no longer says where
a module's data is on a machine, or what a setting's value is. The assignment does, and `plan` shows
it. That is the point, and it is also a real loss of at-a-glance legibility, which the overview has
to give back.
- **Not decided here:** the syntax a definition reads a requirement's fields with; a node's default
layout; the order in which the mechanisms are retired. [To-be 27](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md)
proposes all three.
@@ -169,8 +168,8 @@ On acceptance, each of these is amended by a record of its own, not edited:
| Every requirement has one of the four providers | The parser refuses a requirement whose provider kind is not one of the four. |
| Installation resolves every requirement | A resolution test with one requirement unanswered: refused, naming it and what could answer it. |
| A host requirement is answered on its module's own node | A resolution test: an assignment placing a directory or a port on another node is refused. |
| A module can run twice on one node | A resolution test assigning one module twice under two instance names: two directories, two logins, two containers, separate settings, no collision. |
| A public name already taken is refused | A resolution test: a second instance asking for a public name the first holds is refused, naming the first. |
| 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, naming the existing assignment. |
| A public name already taken is refused | A resolution test: a second assignment asking for a public name another holds is refused, naming the holder. |
| An adopted assignment is placed where its data is | An adoption test: the directory resolves to the data's existing location, and nothing is moved. |
## References
@@ -78,32 +78,54 @@ it first means reordering the whole installation and giving the vault a second w
requiring a database makes the database's provider require a secret for gitea, and the vault
answers it. The provider's own code does not change: it is handed a login and a password, as today;
- a module's **own secret**. `own-secrets` is retired;
- every **broker account**: a module's, a node's, the builder's. The broker delivers `amqp` through
its seat ([ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)), and the broker's own
provisioner creates each account from the vault's secret, like any provider. The controller no
longer creates accounts, and there is no separate command to forget;
- an **enrolment token**;
- every **broker account** on the mesh's bus: a module's, a node agent's, the builder's, the
controller's. The broker holding `mesh-broker` carries the mesh's bus
([ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)), and its own provisioner creates
each account from the vault's secret, like any provider. The controller no longer creates accounts,
and there is no separate command to forget;
- an **enrolment token**. The vault makes it; the operator receives the token, sealed to the operator
key, to hand to the joining machine; the controller receives only what it needs to verify it, never
the token itself;
- a **secret operator value**, such as an external API key, which the operator delivers to the vault
([ADR 0092](0092-an-operator-delivers-a-pair-credential.md)). A licence's credential is one of these.
What the licences context adds, refreshing a token, is provider behaviour, decided in its own record.
What the licences context adds, refreshing a token, is provider behaviour, decided in its own record;
- a **secret a backend issues itself**, such as an API token a forge hands out exactly once when asked.
The vault cannot make that value. The module that received it delivers it to the vault, which keeps
it and provides it like any other; rotating it means asking the backend again.
**Only the vault may provide `secret`.** A module providing it must hold the `mesh-vault` seat, and
the parser refuses one that does not. A pin cannot route a `secret` requirement anywhere else, because
there is nowhere else.
**Parties that are not modules take the same path.** The controller's own store login and bus account,
and each node agent's bus account, have no definition to require them. The controller asks the vault
on its own behalf, or a node's, and the vault answers the way it answers any requirement: made by the
vault, sealed to the recipient, carried by the mesh. The requirement is not written in a definition,
because the controller and a node agent are the mesh itself, but it is answered no differently.
**Only the vault may provide `secret`.** An assignment providing it must hold the `mesh-vault` seat.
The parser refuses a definition that provides it and cannot hold the seat, and a pin cannot route a
`secret` requirement anywhere else, because there is nowhere else.
**A secret has recipients, and the vault delivers to each.** The database credential has two: the
provider, which *applies* it by creating the login, and the consumer, which *presents* it. The vault
hands the value to the mesh sealed to each recipient's node. The controller and the broker carry sealed
values they cannot open.
provider, which *applies* it by creating the login, and the consumer, which *reads* it and presents it
when it connects. The vault hands the value to the mesh sealed to each recipient's node. The controller
and the broker carry sealed values they cannot open.
**Genesis delivers, and the vault adopts.** The foundation's first shared secrets exist before the
vault can run: 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 itself, and the first enrolment token. Genesis
generates these, seals them to the operator key as today, and **delivers them to the vault when the
vault is installed**, through the same path an operator's value takes. From then on the vault holds,
audits and rotates them. Unlike an operator's external key, the vault can make their replacements, so
they are delivered but replaceable. Genesis is the only thing besides the vault that ever generates a
shared secret, once, before the vault exists, and it hands them over.
**Genesis delivers, and the vault adopts.** The vault is built on the shared runtime base, which the
installation makes only after the store, the broker and the controller are running
([to-be 21](../03-DESIGN/01-to-be/21-the-installation-in-full.md)). So the vault is installed **as soon
as that base exists**, before any other module built on it, and everything needed before that moment is
generated by genesis:
- 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.
Until the broker's provisioner runs, genesis creates the bus accounts it generated, with the broker's
admin, as the controller does today. Genesis seals all of it to the operator key, and when the vault is
installed it **delivers the values to the vault, recorded as the mesh's own**, not as an operator's.
That distinction matters: an operator's value is never replaced ([ADR 0092](0092-an-operator-delivers-a-pair-credential.md)),
and these are, because the vault can make their replacements. The broker's provisioner then adopts the
accounts genesis created. From then on the vault makes every shared secret, and genesis has made its
last one.
**A provider makes resources and data, and the mesh carries data back.** A provider's adapter may
answer with its contract's non-secret fields: a site id, a registered name. The mesh delivers them to
@@ -124,9 +146,12 @@ rotated by the vault: rotating it means an operator delivering a new one.
| **reading it at start** | a consumer reading its password when it starts | the host restarts it, or recreates a container whose env-file carries it |
A secret read only when a service first initialises cannot be rotated by a restart. Its contract marks
it applied, and a provisioner makes the change. The host derives which recipients read a secret at
start from the requirement their definition reads it through, so no definition declares a restart for
a secret.
it applied, and a provisioner makes the change. Where no provisioner exists to make it, such as a
module's own bootstrap password, the contract marks the secret **not rotatable by the mesh**, and a
rotation request is refused, saying why, rather than restarting a service that would carry on with
the old value. The host derives which recipients read a secret at start from the requirement their
definition reads it through, so no definition declares a restart for a secret. A provider's
per-consumer secrets are applied, never read at start, so the host never restarts a provider for one.
**It is applier-first.** The vault delivers the new value first to the recipients that apply it. Each
applies it, verifies that the new value authenticates and the old one no longer does, and confirms.
@@ -134,11 +159,20 @@ applies it, verifies that the new value authenticates and the old one no longer
message costs one pass. Only after every applier has confirmed does the vault release the value to the
recipients that read it at start.
**The remaining window is stated.** If an applier applies the new value and its provisioner stops
before confirming, the recipients that present the secret are locked out until the provisioner runs
again, because 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 is bounded by that restart. The mesh shows the
rotation as waiting on that applier for as long as it lasts, never as done.
**Open: keeping readers from being locked out.** Review found three cases this rule does not survive:
- a reader whose machine is offline when an applier has already applied the new value is locked out
until it returns, where to-be 13 would have refused the rotation and kept the old value working;
- a bus account's owner can be locked out permanently, because the confirmation and the new value
travel over the bus it has just lost;
- a provisioner restarted mid-rotation no longer knows the old value, so it cannot verify that the old
value has stopped working.
Two answers are recorded, and one must be chosen before this record is accepted. **Overlap:** an
applier keeps the old and new credential valid together until every reader has confirmed the new one,
for instance by alternating between two derived logins with the adapter's existing create and remove,
which needs no change on the consumer's side. Or **re-confirm with safeguards:** a pre-check that every
reader is reachable before any applier starts, plus a special path for bus accounts.
**It is confirmed.** A rotation is shown as unconfirmed until every applier has confirmed, and every
recipient that reads at start has restarted with the new value and passed its health check, where its
@@ -164,13 +198,17 @@ On acceptance, each of these is superseded or amended by this record, not edited
secrets are retired, and its rejection of vault-only minting is answered by genesis delivering the
first secrets. "The vault stores no plaintext, ever" stands.
- [ADR 0092](0092-an-operator-delivers-a-pair-credential.md) is amended: an operator delivers a secret
to the vault, and genesis delivers the foundation's first secrets the same way.
to the vault. Genesis's values reach the vault by delivery too, but are recorded as the mesh's own,
so 0092's rule that an operator's value is never replaced does not apply to them.
- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md) is amended: a broker
account is created by the broker's provisioner, not the controller. Its scoping stands.
- [To-be 13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md),
[to-be 21](../03-DESIGN/01-to-be/21-the-installation-in-full.md) and
[to-be 24](../03-DESIGN/01-to-be/24-the-secrets-vault.md) are amended: rotation is applier-first,
derived and confirmed; genesis delivers its secrets to the vault; the vault is the only maker.
derived and confirmed; the vault is installed as soon as the shared runtime base exists, and genesis
delivers its secrets to it; the vault is the only maker.
- The [glossary](../00-META/glossary.md) gains *shared secret*, *recipient*, *applies* and *reads at
start*, and *reserved provision*, once this record is accepted.
- [Issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)
becomes a prerequisite: rotation cannot be trusted while a changed env-file leaves a container on
its old value.
@@ -185,24 +223,30 @@ On acceptance, each of these is superseded or amended by this record, not edited
adapter is unchanged. A data provider's adapter gains a return value.
- The broker's provisioner gains every bus account, and the controller loses five separate places it
generates a secret today.
- 54 modules move from own secrets to vault requirements.
- **What got harder:** a rotation waits for its appliers, and an applier that stops mid-rotation locks
presenters out until it restarts. Both are shown, not hidden, and the second is bounded by a
supervised restart rather than by someone noticing.
- 54 modules move from own secrets to vault requirements. Six provider clients export a password
generator nothing uses any more; it is removed, so no module can quietly start minting again.
- The installation changes order: the vault is installed as soon as the shared runtime base exists,
before any other module built on it.
- **What got harder:** a rotation waits for its appliers, and how a reader is kept from being locked
out while it does is still open (above). A secret some services read only at first start can no
longer be "rotated" by a restart that quietly changes nothing; it is refused instead.
## How it is checked
| Rule | Checked by |
|---|---|
| Only the vault generates a shared secret after genesis | A controller test: no code path generates a shared secret. An installer test: genesis generates exactly the foundation's first secrets and delivers them to the vault. |
| A private key is made where it is used | A test per key: a node's sealing key never leaves the node, and the operator's private key never enters the mesh. |
| Only the vault provides `secret` | The parser refuses a module providing `secret` without holding `mesh-vault`, and resolution refuses a pin on a `secret` requirement. |
| Only the vault generates a shared secret after genesis | A controller test: no code path generates a shared secret. A catalogue test: no module's code generates one, found by scanning for generation calls, with none exempt. An installer test: genesis generates exactly the list above and delivers it to the vault, recorded as the mesh's own. |
| A private key is made where it is used | A test per key: a node's sealing key never leaves the node, the operator's private key never enters the mesh, and the certificate authority's private key never leaves the controller's identity store. |
| Only the vault provides `secret` | The parser refuses a definition providing `secret` that cannot hold `mesh-vault`, and resolution refuses a pin on a `secret` requirement. |
| Parties that are not modules take the same path | Controller tests: its own store login, its bus account and a node agent's bus account are each made by the vault and delivered sealed; an enrolment token reaches the controller only as what verifies it. |
| A backend-issued secret enters through the vault | A vault test: a value delivered as issued is provided like any other, and rotating it is refused as the vault's act. |
| A secret with no provisioner to apply it is not rotated by restart | A vault test: rotating a secret marked not rotatable by the mesh is refused, naming why. |
| A provider's per-consumer secret comes from the vault | A resolution test: a consumer requiring a database expands to a secret requirement for it, answered by the vault and delivered to both recipients. |
| Values are carried sealed | A controller test: each recipient's copy opens with that recipient's node key and no other; neither the controller nor a message on the broker can open one. |
| Own secrets are retired | A catalogue test: no definition declares an own secret, with a declared list of exceptions that shrinks to empty. |
| Bus accounts come from the broker's provisioner | A resolution test: assigning a module that speaks on the bus yields its account, created by the broker's provisioner with no separate command. |
| An irreplaceable delivered value is not rotated by the vault | A vault test: a rotation request on an operator's external key is refused, naming the operator as its source. |
| Rotation is applier-first and re-confirmed | A rotation test: presenters are not sent the new value until every applier confirms, and a confirmation lost in transit is repeated on the next pass. |
| An operator's value is never rotated by the vault, and genesis's values are | Vault tests: a rotation request on an operator's external key is refused, naming the operator; the same request on a value genesis delivered makes a replacement. |
| Rotation is applier-first | A rotation test: readers are not sent the new value until every applier confirms. How lock-out is prevented is open, and its check is written when that is decided. |
| Restarts are derived from how a secret is read | A host test: a secret read at start restarts its reader, and recreates a container whose env-file carries it; an applied secret restarts nothing. |
| Rotation is confirmed | A rotation test: an applier confirms only after the new value authenticates and the old one does not, and the rotation shows unconfirmed until every reader restarted and passed its health check. |
| A provider answers data back | A lab test with a consumer requiring analytics: the provider's site id reaches it as a resolved value. |
+2 -2
View File
@@ -156,7 +156,7 @@ python3 00-META/checks/index.py fail if stale
- **0087** — [A seeded file is created once, and what grows in it is not the mesh's](0087-a-seeded-file-is-created-once.md)
- **0091** — [A mount is declared, and there are three things it can be](0091-a-mount-is-declared-three-ways.md)
- **0099** — [A step that runs once names what it reads, and runs again when it changed](0099-a-step-that-runs-once-names-what-it-reads.md)
- **0110** — [A seat is a module assignment from a closed set, and it may deliver a provision](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)
- **0110** — [A seat is held by one assignment, from a closed set, and it may deliver a provision](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) *(proposed)*
- **0112** — [A module definition names no node, no mesh and no path: everything it needs is a requirement the mesh resolves](0112-a-module-definition-names-no-node-mesh-or-path.md) *(proposed)*
- **0113** — [The vault makes every shared secret, a provider makes resources and data, and the mesh carries both](0113-the-vault-makes-every-secret.md) *(proposed)*
@@ -178,7 +178,7 @@ python3 00-META/checks/index.py fail if stale
- **0096** — [An upstream image is copied between registries, never through a machine's image store](0096-an-upstream-image-is-copied-between-registries.md)
- **0097** — [A vendor image is a declared build input, and a recipe fetches nothing undeclared](0097-a-vendor-image-is-a-declared-build-input.md)
- **0107** — [Persistent data is a directory bind, never a named volume](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)
- **0111** — [A build source is on the mesh's git seat, or it is an external repository](0111-a-build-source-is-on-the-git-seat-or-external.md)
- **0111** — [A build source is on the mesh's git seat, or it is an external repository](0111-a-build-source-is-on-the-git-seat-or-external.md) *(proposed)*
### How it is checked
+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.