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 ## How modules relate to the mesh
- **seat** — a named role at a scope (node / site / mesh), held by a module assignment, from a - **seat** — a named position at a scope (node / site / mesh) with a **capacity**. A capacity-1 seat
**closed set** the mesh defines: a claim naming a seat outside the set is refused. A seat may is exclusive (one holder); a higher-capacity seat is a **bench** (several holders coexist).
**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).
- **claim** — a module taking a spot on a seat. `claims: [{name, scope}]` in a manifest. A - **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 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 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)). 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 - **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 and wires the two with an endpoint and a credential. This is separate from seats: a provision is
is a role you occupy, and the two meet where a seat delivers a provision: occupying the seat is a service you offer, a seat is a slot you occupy.
what makes a module *the* provider of it.
## How this page is kept ## How this page is kept
@@ -1,20 +1,20 @@
--- ---
topic: what runs on it topic: what runs on it
status: accepted status: proposed
date: 2026-09-25 date: 2026-09-25
deciders: jochen deciders: jochen
reconstructed: false reconstructed: false
extends: 0009-modules-and-the-graph.md 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 ## Context
[ADR 0009](0009-modules-and-the-graph.md) introduced claims: a module declares something [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) 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 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 **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. 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 **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. 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 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 every manifest in two repositories, because the controller's own manifest lives in its own
code, because one module it ships has its manifest composed there. While this repository ([ADR 0069](0069-a-module-is-a-repository-and-a-path.md)), and then the controller's code,
record was being prepared, that enumeration was done by hand, and it missed both of the last two because one module it ships has its manifest composed there. While this record was being prepared,
sources: eleven claims were reported where there are thirteen. 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.** **A claim in a definition makes a module singular, not a role.** The store module's definition
Of the thirteen claims, four are held by a module that provides something consumers require: claims `mesh-store`, so every assignment of it claims the seat, and a second store module on any other
`mesh-store` (`postgres-database`, eleven consumers), `mesh-broker` (`amqp`), `the-artifact-store` node is refused. What is singular is *the store the mesh itself uses*, not postgres. Any module can
(`artifact-store`) and `the-dns-port`. The rest deliver nothing to anybody, and are still run on any node whose capabilities match, which is a core principle of the module system, and a claim
meaningful: they say which module is this mesh's packet filter, or resolver configuration. written into the definition breaks it for every module that claims anything.
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) **Some seats are the mesh's one of something everyone consumes, and nothing uses that fact.** A
anticipates exactly that case — gitea and verdaccio both answering npm — and under today's requirement for a mesh-scoped provision with more than one provider is refused until a person pins,
resolution it would mean a pin on every machine that builds anything. **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 ## Considered Options
**1. Leave seats as free-form exclusion, and keep provisions as the only thing that delivers.** **1. Leave seats as free-form exclusion, claimed in definitions.** Rejected. The overview stays
Rejected. The overview stays unanswerable, and a second provider of anything costs a pin per unanswerable, a module that claims a seat can run on only one node, and a second provider of anything
consumer node — the decision "gitea is our npm registry" made again on every machine. costs a pin per consumer node.
**2. Two concepts: seats for exclusion, and a new word for the mesh's one consumable thing.** **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 Rejected. Both mean "this mesh's one X". Every existing claim would first have to be classified into
into one or the other, and the overview a person wants is one list, not two. 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. Chosen.
## Decision ## Decision
**The mesh defines a closed set of seats.** Each entry has a name, a scope, what occupying it **The mesh defines a closed set of seats.** Each entry has a name, a scope, what holding it delivers
delivers (if anything), and the decision that made it a seat. A claim naming a seat outside the set (if anything), and the decision that made it a seat. A seat outside the set is refused wherever it
is refused, and so is a claim at a scope other than the seat's. Adding a seat is a decision, for the is named. Adding a seat is a decision, for the same reason adding a shape to the host's vocabulary is
same reason adding a shape to the host's vocabulary is one: the set is what a person reads to learn one: the set is what a person reads to learn what a mesh can have, and a name added without an
what a mesh can have, and a name added without an argument is a name nobody can explain later. 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 **A definition says which seats a module *can* hold. An assignment says which it *does* hold.** The
about that assignment: its node, the node's settings for it, and what it serves. Holdings are not store module can hold `mesh-store`, and it may be assigned to every node. Exactly one of those
stored separately. The seat points at an assignment, and a second record of the same fact would be a assignments holds the seat, because that assignment said so. A second assignment saying so, at the
second thing to disagree with the first. 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 **What the mesh knows about a seat's holder is what it knows about that assignment**: its node, the
provision can only be held by a module that provides it, at the seat's scope, and a claim that does node's settings for it, and what it serves. Holdings are not stored separately. The seat points at an
not is refused. A requirement for that provision resolves, in order, to: 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 **Holding a seat may deliver a provision.** A seat that delivers a provision may only be held by an
contents, as [to-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md) already allows; assignment of a module that provides it, at the seat's scope. A requirement for that provision
2. **the holder of the seat** that delivers it, **even when another provider runs on the consumer's resolves, in order, to:
own node**;
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. 3. otherwise refused, naming the unheld seat.
**Co-location does not apply to a provision a seat delivers.** For every other provision, a provider **Co-location does not apply to a provision a seat delivers.** A seat exists to say *which one is the
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
mesh's*, and co-location answering first would let any second provider on a consumer's machine take 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) 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 **A seat delivers a provision only where the mesh has one answer for everyone.** The artifact store,
never guessed. The seat is not a guess. It is the choice made once, mesh-wide, by assigning the holder, the npm registry, git and the vault are each one per mesh by their own records, so their seats
instead of once per consumer by pinning. A second provider may run beside the holder, and whatever deliver them.
requires the provision still resolves to the holder without anybody naming it.
**Seats are also informational.** The controller lists every seat in the set, what it delivers, **The foundation's seats deliver nothing.** `mesh-controller`, `mesh-store` and `mesh-broker` name
and which assignment holds it, including seats nobody holds. An unheld seat is an answer, "this mesh which assignment the mesh *itself* uses: the controller, the store holding its records, the broker
has no X", not an error. 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 **A seat may reserve its provision.** Where a second provider would break the reason the provision
decision about the provision, not about the seat. The broker, the artifact store, the npm registry, exists, only an assignment holding the seat may provide it at all: the parser refuses a definition
git and the vault are each one per mesh by their own records, so their seats deliver them. The store that provides it without being able to hold the seat, resolution refuses an assignment providing it
is not: [to-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md) has each node running its own without holding the seat, and a pin cannot choose anyone else. `secret` is the one reserved provision.
stores, with a consumer served by the one on its own machine, and the foundation's store is the The vault is one per mesh because a second *"would be a second place to lose"*
controller's own memory, provider to nobody ([to-be 21](../03-DESIGN/01-to-be/21-the-installation-in-full.md)). ([ADR 0085](0085-a-secret-is-a-provision.md), as amended), and a second `secret` provider is exactly
So `mesh-store` guards that the foundation's store is singular, and delivers nothing. Were it to that, whether a pin chose it or not.
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 a rule the provision exists **Seats are also informational.** The controller lists every seat in the set, what it delivers, and
for, only the seat's holder may provide it at all: the parser refuses anyone else, and a pin cannot which assignment holds it, including seats nobody holds. An unheld seat is an answer, "this mesh has
choose anyone else. `secret` is the one reserved provision. The vault is one per mesh because a second no X", not an error.
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.
**The first set is the twelve seats already claimed, plus two.** Thirteen claims are in use, and **The first set is the twelve seats already claimed, plus two.** Thirteen claims are in use, and they
they name twelve seats because two alternative modules claim `the-resolver-configuration`. This name twelve seats because two alternative modules claim `the-resolver-configuration`. This record
record admits every seat the catalogue and the controller claim today, so no module is refused by admits every seat the catalogue and the controller claim today, so no definition is refused by it:
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-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-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-broker` | mesh | — | `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-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-artifact-store` | mesh | `artifact-store` | `distribution` | [0075](0075-two-stores-and-which-provides-what.md) |
| `the-catalogue` | mesh | — | `mesh-catalog` | this record | | `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) | | `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 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. 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 A gitea assignment holds it. verdaccio provides the same provision and cannot hold the seat, so it is
provider this record exists to make harmless. 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 `mesh-vault` answers [issue 106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md). The vault is one
is one per mesh ([ADR 0085](0085-a-secret-is-a-provision.md), as amended), and until now that was per mesh ([ADR 0085](0085-a-secret-is-a-provision.md), as amended), and until now that was enforced by
enforced by nothing. A second vault would have answered requirements silently, and any consumer on nothing. The seat is named after its server, by the 0079 convention.
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.
`the-dns-port` is listed as delivering nothing, although `dnsmasq` provides `wildcard-resolution`. `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 That provision is node-scoped and answered on the machine, so no preference between providers arises.
arises. Whether the seat should say it delivers it is left for when a second resolver makes the
question real. ## 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 ## Consequences
- The controller carries the set in code. A test asserts its size, and that every entry names the - 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. 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. - An assignment gains the seats it holds. Genesis assigns the foundation's store, broker and
- Manifest validation refuses an unknown seat, a seat claimed at the wrong scope, and a controller holding their seats, where today their definitions claim them.
provision-delivering seat claimed by a module that does not provide the provision. The three - Manifest validation refuses an unknown seat, a seat named at the wrong scope, and a delivering seat
refusals name the seat and the set. named by a module that does not provide the provision. Resolution refuses a second holder, and an
- Resolution prefers the seat's holder among several providers, after a pin. A provider record assignment holding a seat its module cannot hold.
gains the module it came from, because two modules on one node could otherwise not be told apart - Resolution prefers the seat's holder for a provision it delivers, after a pin. A provider record
as holder and non-holder. gains the module it came from.
- A `seats` command lists the set with each seat's holders, derived from assignments. - A `seats` command lists the set with each seat's holder, 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).
- **What got harder:** a module wanting a new singular role can no longer invent a name. It needs a - **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. record. And an assignment has one more thing to say. Both are the point.
- **Not changed:** `${seat:<seat>:<port>}` stays as it is. It exists so the controller can reach a - **Not changed:** the controller's seat placeholder stays as it is. It exists so the controller can
foundation it made before any module existed, and it cannot be a consumer. A module that needs reach a foundation it made before any module existed.
something from a seat's holder requires the provision the seat delivers, and receives it the way
any provision is received: through a grant.
## How it is checked ## How it is checked
| Rule | Checked by | | 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. | | 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. | | 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 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)). | | 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)). |
| 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 is held by one assignment, not by a module | A resolution test: the store module assigned to two nodes resolves, with one assignment holding `mesh-store`; a second assignment asking to hold it is refused. |
| A 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`. | | 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. |
| 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. | | 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 ## References
- [ADR 0009](0009-modules-and-the-graph.md): claims, scopes, and "refused, never guessed" - [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 0079](0079-the-foundation-seats-are-named-after-their-servers.md): a seat named after what it is
- [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md): the per-ecosystem registry seats - [ADR 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`), - `mesh-controller internal/catalogue/resolve.go` (`checkClaims`, the brokered branch of `Resolve`),
`internal/catalogue/manifest.go` (claim validation), `cmd/mesh-controller/plan.go` (holdings) `internal/catalogue/manifest.go` (claim validation), `cmd/mesh-controller/plan.go` (holdings)
@@ -1,6 +1,6 @@
--- ---
topic: building it topic: building it
status: accepted status: proposed
date: 2026-09-25 date: 2026-09-25
deciders: jochen deciders: jochen
reconstructed: false 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 **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`, [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.** **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 - `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 recorded source keeps the seat form; the build log keeps the URL that was actually cloned, because
that is what happened. 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 - **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 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` 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 **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 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 **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, 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 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. no mount at a machine-identical path.
**An assignment has an identity of its own: an instance name**, defaulting to the module's name. **A module is assigned at most once to a node.** An assignment is a module on a node, and that pair is
Everything keyed by the module's name today is keyed by the instance instead: directories, container its identity: its directories, containers, login, broker account and settings are keyed by it, as
names, the login a consumer presents, broker accounts, a claim's holder, the settings an assignment they are today. A module may run on many nodes, and one of those assignments may hold a seat
carries, and a provider's identity. So **one module may be assigned to one node more than once.** What ([ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)). Running the same module twice
must stay singular stays so by a claim, or by an operator value colliding: a public name already taken on one machine is not supported. The cases that seemed to need it, such as two stores of one engine or
is refused like any other singular thing. 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, **What must stay singular stays so** by a seat, or by an operator value colliding: a public name
so the foundation's requirements are answered the same way as everything else already held by another assignment is refused like any other singular thing.
([ADR 0113](0113-the-vault-makes-every-secret.md)).
**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 ## What this changes in earlier records
On acceptance, each of these is amended by a record of its own, not edited: 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 - [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 - [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. 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 - [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. 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, - [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. 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 - 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 requirement answered by any of the four providers, and *requirement* and *contract* are added. None
record is only proposed, because the glossary is the authority on the words in use, not on words of it lands while this record is only proposed, because the glossary is the authority on the words
under review. in use, not on words under review.
## Consequences ## 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 - 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 answers directories and ports. The settings, placeholders, facts and bindings that exist today
are retired as separate mechanisms, once nothing uses them. are retired as separate mechanisms, once nothing uses them.
- Identity moves from the module to the instance, which touches logins, broker accounts, settings, - Identity stays a module on a node. Nothing is renamed, and a login still fits the tightest backend
provider selection and every resource name. A login already has a 20-character limit as [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md) arranges.
([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md)), which a node and an instance - **What got harder:** one module cannot run twice on one machine; a second stage or a second store
name will strain. The design must answer that before a second instance is possible. of one engine is a different module or a different machine. And a definition no longer says where
- **What got harder:** a definition no longer says where a module's data is on a machine, or what a a module's data is on a machine, or what a setting's value is. The assignment does, and `plan` shows
setting's value is. The assignment does, and `plan` shows it. That is the point, and it is also a it. That is the point, and it is also a real loss of at-a-glance legibility, which the overview has
real loss of at-a-glance legibility, which the overview has to give back. to give back.
- **Not decided here:** the syntax a definition reads a requirement's fields with; a node's default - **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) 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. 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. | | 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. | | 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 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 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 instance asking for a public name the first holds is refused, naming the first. | | 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. | | 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 ## 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 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; 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; - 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 - every **broker account** on the mesh's bus: a module's, a node agent's, the builder's, the
its seat ([ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)), and the broker's own controller's. The broker holding `mesh-broker` carries the mesh's bus
provisioner creates each account from the vault's secret, like any provider. The controller no ([ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)), and its own provisioner creates
longer creates accounts, and there is no separate command to forget; each account from the vault's secret, like any provider. The controller no longer creates accounts,
- an **enrolment token**; 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 - 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. ([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 **Parties that are not modules take the same path.** The controller's own store login and bus account,
the parser refuses one that does not. A pin cannot route a `secret` requirement anywhere else, because and each node agent's bus account, have no definition to require them. The controller asks the vault
there is nowhere else. 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 **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 provider, which *applies* it by creating the login, and the consumer, which *reads* it and presents it
hands the value to the mesh sealed to each recipient's node. The controller and the broker carry sealed when it connects. The vault hands the value to the mesh sealed to each recipient's node. The controller
values they cannot open. and the broker carry sealed values they cannot open.
**Genesis delivers, and the vault adopts.** The foundation's first shared secrets exist before the **Genesis delivers, and the vault adopts.** The vault is built on the shared runtime base, which the
vault can run: the store's superuser, the broker's admin in the hashed form the broker needs, the bus installation makes only after the store, the broker and the controller are running
accounts of the temporary controller and of the vault itself, and the first enrolment token. Genesis ([to-be 21](../03-DESIGN/01-to-be/21-the-installation-in-full.md)). So the vault is installed **as soon
generates these, seals them to the operator key as today, and **delivers them to the vault when the as that base exists**, before any other module built on it, and everything needed before that moment is
vault is installed**, through the same path an operator's value takes. From then on the vault holds, generated by genesis:
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 - the store's superuser, and the broker's admin in the hashed form the broker needs;
shared secret, once, before the vault exists, and it hands them over. - 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 **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 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 | | **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 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 it applied, and a provisioner makes the change. Where no provisioner exists to make it, such as a
start from the requirement their definition reads it through, so no definition declares a restart for module's own bootstrap password, the contract marks the secret **not rotatable by the mesh**, and a
a secret. 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 **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. 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 message costs one pass. Only after every applier has confirmed does the vault release the value to the
recipients that read it at start. recipients that read it at start.
**The remaining window is stated.** If an applier applies the new value and its provisioner stops **Open: keeping readers from being locked out.** Review found three cases this rule does not survive:
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 - a reader whose machine is offline when an applier has already applied the new value is locked out
supervised and restarted when it exits, so the window is bounded by that restart. The mesh shows the until it returns, where to-be 13 would have refused the rotation and kept the old value working;
rotation as waiting on that applier for as long as it lasts, never as done. - 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 **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 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 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. 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 - [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 - [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. 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 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 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, [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) - [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 becomes a prerequisite: rotation cannot be trusted while a changed env-file leaves a container on
its old value. 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. 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 - The broker's provisioner gains every bus account, and the controller loses five separate places it
generates a secret today. generates a secret today.
- 54 modules move from own secrets to vault requirements. - 54 modules move from own secrets to vault requirements. Six provider clients export a password
- **What got harder:** a rotation waits for its appliers, and an applier that stops mid-rotation locks generator nothing uses any more; it is removed, so no module can quietly start minting again.
presenters out until it restarts. Both are shown, not hidden, and the second is bounded by a - The installation changes order: the vault is installed as soon as the shared runtime base exists,
supervised restart rather than by someone noticing. 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 ## How it is checked
| Rule | Checked by | | 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. | | 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, and the operator's private key never enters the mesh. | | 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 module providing `secret` without holding `mesh-vault`, and resolution refuses a pin on a `secret` requirement. | | 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. | | 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. | | 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. | | 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. | | 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. | | 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 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. | | 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. | | 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. | | 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. | | 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) - **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) - **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) - **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)* - **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)* - **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) - **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) - **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) - **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 ### How it is checked
+3 -15
View File
@@ -2,11 +2,10 @@
layer: to-be layer: to-be
status: designed status: designed
code: [] code: []
updated: 2026-09-25 updated: 2026-09-20
decisions: decisions:
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md - 02-DECISIONS/0084-which-provider-serves-a-consumer.md
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.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 # 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 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. 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 **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 named, and none is co-located, the requirement is unsatisfiable and is refused with the candidates
with the candidates shown — the same stance shown — the same stance
[ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) took [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 against a confidently-wrong match, applied to the instance rather than the dialect. A wrong answer
delivered quietly costs more than a refusal. delivered quietly costs more than a refusal.
+56 -54
View File
@@ -1,6 +1,6 @@
--- ---
layer: to-be layer: to-be
status: in-progress status: proposed
code: code:
- mesh-controller internal/catalogue/seats.go - mesh-controller internal/catalogue/seats.go
- mesh-controller internal/catalogue/resolve.go - mesh-controller internal/catalogue/resolve.go
@@ -17,8 +17,8 @@ decisions:
# 26 — The seats # 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 **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. Occupying one may deliver a provision, and the 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". list of seats with their holders is the quickest answer to "what is in this mesh".
## What a seat is ## What a seat is
@@ -27,28 +27,31 @@ A seat has four properties, fixed by the mesh rather than by any module:
| property | is | | 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 | | 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 | | delivers | the provision its holder answers for, or nothing |
| decision | the record that made it a seat | | 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 **A definition says which seats a module can hold. An assignment says which it does hold.** The store
satisfied by assigning the module somewhere. The seat is not a second record beside the assignment. module can hold `mesh-store`, and it may run on every node whose capabilities match. Exactly one of
It points at the assignment, and everything the mesh knows about the holder is what it knows about those assignments holds the seat, because that assignment says so, and a second assignment saying so
that assignment: the node, the node's settings for the module, and what the module serves. 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 seat points at the assignment.** Everything the mesh knows about the holder is what it knows
the wrong scope. Adding a seat is a decision, recorded, for the reason every addition to the host's about that assignment: the node, the node's settings for the module, and what the module serves.
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 is closed.** A seat the mesh does not define is refused wherever it is named, and so is one
named at the wrong scope. Adding a seat is a decision, recorded, for the reason every addition to the
host's vocabulary is one: the set is what a person reads to learn what a mesh can have, and an entry
nobody argued for is an entry nobody can explain.
## The set ## The set
| seat | scope | delivers | typically held by | | seat | scope | delivers | typically held by |
|---|---|---|---| |---|---|---|---|
| `mesh-controller` | mesh | — | the controller | | `mesh-controller` | mesh | — | the controller |
| `mesh-store` | mesh | — | the foundation's store | | `mesh-store` | mesh | — | the store the mesh's own records live in |
| `mesh-broker` | mesh | `amqp` | the broker | | `mesh-broker` | mesh | — | the broker carrying the mesh's own bus |
| `mesh-vault` | mesh | `secret`, reserved | the vault | | `mesh-vault` | mesh | `secret`, reserved | the vault |
| `the-artifact-store` | mesh | `artifact-store` | the artifact registry | | `the-artifact-store` | mesh | `artifact-store` | the artifact registry |
| `the-catalogue` | mesh | — | the catalogue | | `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 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) 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 govern, and code that disagrees is what is wrong.** The implementation in progress predates several
things here: the `mesh-vault` seat and its reservation, and the rule that `mesh-store` delivers things here: seats held by assignments rather than claimed by definitions, the `mesh-vault` seat and
nothing. It is brought to this table before it merges. 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
A seat that delivers a provision may only be held by a module that provides it, at the seat's scope. **A seat delivers a provision only where the mesh has one answer for everyone.** The artifact store,
A mesh seat delivers a mesh-scoped provision. the npm registry, git and the vault are each one per mesh by decision. A seat that delivers a
provision may only be held by an assignment of a module that provides it, at the seat's scope.
**A 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.
**Its holder answers for that provision.** A requirement for it resolves, in order, to: **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 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 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 machine take over for that consumer, silently. So a second provider can run beside the holder and
harm nothing. The forge holds harm nothing. A forge assignment holds `npm-package-registry`. An npm proxy may provide the same
`npm-package-registry`. An npm proxy may provide the same provision on another machine, and a module provision on another machine, and a module requiring an npm registry is still served by the forge,
requiring an npm registry is still served by the forge, without anybody pinning it. without anybody pinning it.
**Moving the role is changing which module claims the seat, and today that is a definition change.** **Moving the role is changing which assignment holds the seat.** No definition changes and nothing is
A claim is part of a module's definition, so the proxy's definition must claim the seat and the unassigned: the forge keeps running, and keeps holding `git`, when its npm role moves. A module can
forge's must stop. The forge cannot simply be unassigned, because it holds `git` as well. Every take the role only if its definition says it can hold the seat.
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.
**What a consumer receives is a grant**, the same as for any provision: where the provider answers, **The vault's provision is reserved.** Only an assignment holding `mesh-vault` may provide `secret` at
what it serves, and a credential. A consumer never reads the seat directly. The one exception is the all: a definition providing it that cannot hold the seat is refused, an assignment providing it without
controller itself, which reaches the store and the broker through a narrow seat placeholder, 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 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) 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. 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 overview
The controller lists every seat in the set with its scope, what it delivers, and each holder as a The controller lists every seat in the set with its scope, what it delivers, and its holder as a node
node and a module. A seat nobody holds is listed as unheld. That is an answer, "this mesh has no and a module. A seat nobody holds is listed as unheld. That is an answer, "this mesh has no forge",
forge", and not a fault. and not a fault.
Holdings are derived from assignments whenever they are asked for, never stored. The list is always 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. 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 | | 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 | | 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, For a repository on the seat, the controller composes the clone URL at the moment of building, from
from where the holder runs and the scheme and port it serves for `git`. The recorded source never where the holder runs and the scheme and port it serves for `git`. The recorded source never contains
contains an address, so moving the forge changes nothing that was recorded. The build machine is not an address, so moving the forge changes nothing that was recorded. The build machine is not told the
told the difference: it receives a URL either way. 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. 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 **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 public. The natural place for a clone credential is a `secret` from the vault, and that is a decision
decision still to take. still to take.
@@ -96,9 +96,17 @@ per consumer, named for that consumer:
Every other shared secret takes the same path: Every other shared secret takes the same path:
- a module's own secret; - a module's own secret;
- every broker account's password, where the broker's own provisioner creates the account; - every broker account's password on the mesh's bus, where the broker's own provisioner creates the
- an enrolment token; account;
- a secret operator value, which the operator delivers to the vault. - 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 **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 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: *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; the assignment says nothing;
- **a placement**, where the assignment puts one directory elsewhere: on a second disk, or where an - **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)). 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 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. 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 **A module is assigned at most once to a node**, and that pair is the assignment's identity
keyed by the module's name today is keyed by the instance: directories, containers, the login it ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md)). Its directories,
presents, its broker account, the seats it holds, its settings and its identity as a provider. 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: **A module may run on many nodes, and one assignment may hold a seat**
by a seat, or by an operator value colliding, as with a public name. ([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 What must stay singular stays so: by a seat, or by an operator value colliding, as with a public name.
([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.
## Genesis ## Genesis
**Genesis delivers, and the vault adopts.** The vault cannot run first: it is built on the shared **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 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 ([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 controller over the bus. So the vault is installed **as soon as that base exists**, before any other
- the store's superuser; module built on it, and genesis generates what is needed until then:
- the broker's admin, in the hashed form the broker needs; - the store's superuser, and the broker's admin in the hashed form the broker needs;
- the bus accounts of the temporary controller and of the vault; - the bus accounts of the temporary and permanent controller, the control-node's agent, the builder,
- the first enrolment token. 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)). Until the broker's provisioner runs, genesis creates those bus accounts with the broker's admin, as the
When the vault is installed, genesis **delivers them to it**, through the same path an operator's value controller does today; the provisioner adopts them when it starts. Genesis seals everything to the
takes. From then on the vault holds, audits and rotates them. It can make their replacements, unlike operator key as today ([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md)), and when the
an operator's external key. 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 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 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 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. declares one. Delivered and working are shown as different things.
**The remaining window is stated.** An applier whose provisioner stops after applying and before **Open: keeping readers from being locked out.** Steps 2 and 3 leave a reader locked out when its
confirming leaves the recipients that read at start locked out: the old value no longer works, and machine is offline after an applier applied, when the secret is a bus account whose owner loses the bus
they have not been sent the new one. A provisioner is supervised and restarted when it exits, so the it would hear the new value on, or when a restarted provisioner can no longer check the old value.
window lasts until that restart. The rotation shows as waiting on that applier throughout, never as done. [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 ## Refusing
@@ -258,10 +276,11 @@ as waiting, and nothing is delivered until the answer arrives.
| mechanism | becomes | | mechanism | becomes |
|---|---| |---|---|
| provisions read through bindings | a module requirement; its answer is the contract's fields | | 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 | | a port the mesh assigns | a host requirement |
| machine facts and machine placeholders | host requirements | | 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 | | a separate command issuing a broker account | a requirement resolved on assignment |
| `restart-on` naming a secret's file | a restart the host derives | | `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 | | 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 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 consumer of analytics receives its site id, and a database credential rotates applier-first, with
the consumer restarted by derivation and the rotation confirmed. the consumer restarted by derivation and the rotation confirmed.
3. **Definitions move.** Every catalogue definition is rewritten, adopted and running assignments 3. **Definitions move, and seats move to assignments.** Every catalogue definition is rewritten, adopted
placed where their data already is. *Ends when* the list of definitions using an old form is and running assignments placed where their data already is, and each claim becomes a seat the
empty, and the old forms are removed. module can hold, held by the assignment that holds it today. *Ends when* the list of definitions
4. **Instances.** The instance name and its short form, and everything keyed by it. *Ends when* one using an old form is empty, the old forms are removed, and the store module runs on two lab
module runs twice on one lab machine with two public names. machines with one holding `mesh-store`.
## How it is checked ## 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. | | 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. | | 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 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. | | 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. |
| 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 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. | | 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 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. | | 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 ## Not settled here
- The exact spelling of the one form. It must name a requirement and a field and nothing else. - 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 - 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. 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 - **How rotation keeps a recipient from being locked out.** Under review: see
definition change ([26 — The seats](26-the-seats.md)). By this document's own logic it belongs to [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), rotation.
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.