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