diff --git a/00-META/glossary.md b/00-META/glossary.md index 4ffe66e..7d38f6d 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -44,22 +44,15 @@ another — and a mesh you cannot name precisely is a mesh two people describe d ## How modules relate to the mesh -- **seat** — a named role at a scope (node / site / mesh), held by a module assignment, from a - **closed set** the mesh defines: a claim naming a seat outside the set is refused. A seat may - **deliver a provision**, and its holder is then the mesh's answer for it when several modules - provide it ([ADR 0110](../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)). - The set, with who holds each seat, is the overview of what a mesh has - ([26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md)). A seat has a **capacity**: a - capacity-1 seat is exclusive (one holder); a higher-capacity seat is a **bench** (several holders - coexist). +- **seat** — a named position at a scope (node / site / mesh) with a **capacity**. A capacity-1 seat + is exclusive (one holder); a higher-capacity seat is a **bench** (several holders coexist). - **claim** — a module taking a spot on a seat. `claims: [{name, scope}]` in a manifest. A mesh-scoped exclusive claim is how the mesh says "there is one of me". A foundation seat is named after the server it guards: the `mesh-controller`, `postgres` and `lavinmq` modules claim the `mesh-controller`, `mesh-store` and `mesh-broker` seats ([ADR 0079](../02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md)). - **provision** — a service one module `provides` and others `require`; the mesh resolves a provider - and wires the two with an endpoint and a credential. A provision is a service you offer, a seat - is a role you occupy, and the two meet where a seat delivers a provision: occupying the seat is - what makes a module *the* provider of it. + and wires the two with an endpoint and a credential. This is separate from seats: a provision is + a service you offer, a seat is a slot you occupy. ## How this page is kept diff --git a/02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md b/02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md index b55badc..6bfbbb9 100644 --- a/02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md +++ b/02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md @@ -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::}` 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) diff --git a/02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md b/02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md index b021ea5..44b7057 100644 --- a/02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md +++ b/02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md @@ -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` diff --git a/02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md b/02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md index 56449e6..8d68e3e 100644 --- a/02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md +++ b/02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md @@ -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 diff --git a/02-DECISIONS/0113-the-vault-makes-every-secret.md b/02-DECISIONS/0113-the-vault-makes-every-secret.md index fe69170..a5b5769 100644 --- a/02-DECISIONS/0113-the-vault-makes-every-secret.md +++ b/02-DECISIONS/0113-the-vault-makes-every-secret.md @@ -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. | diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index bd8635a..70b2bcb 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -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 diff --git a/03-DESIGN/01-to-be/23-choosing-a-provider.md b/03-DESIGN/01-to-be/23-choosing-a-provider.md index ddb6670..50b3ed3 100644 --- a/03-DESIGN/01-to-be/23-choosing-a-provider.md +++ b/03-DESIGN/01-to-be/23-choosing-a-provider.md @@ -2,11 +2,10 @@ layer: to-be status: designed code: [] -updated: 2026-09-25 +updated: 2026-09-20 decisions: - 02-DECISIONS/0084-which-provider-serves-a-consumer.md - 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md - - 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md --- # 23 — Choosing a provider @@ -51,20 +50,9 @@ provider on a different node. That coupling is exactly what may not be guessed, names the provider. Naming it is also what makes a later move safe — the mesh knows the binding is to that provider and not to whichever one is nearest. -**Some provisions have one provider for the whole mesh, and a seat names it.** Where a seat delivers -the provision, its holder answers for it, **and co-location does not apply**: a second provider on the -consumer's own machine does not take over for that consumer. That is not picking: the choice was made -once, mesh-wide, by assigning the holder, rather than once per consumer by naming it -([ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md), -[26 — The seats](26-the-seats.md)). A named provider still wins over the seat, because a consumer -coupled to particular contents has said so, except for `secret`, which only the vault may provide. -Only provisions the design makes one-per-mesh are delivered by a seat: the broker, the artifact store, -a package registry, git and the vault. A database is not. Node-local stores, served by co-location, -are the rule above. - **Ambiguity is refused, never resolved by picking.** If several providers of a kind exist, none is -named, none is co-located, and no seat delivers it, the requirement is unsatisfiable and is refused -with the candidates shown — the same stance +named, and none is co-located, the requirement is unsatisfiable and is refused with the candidates +shown — the same stance [ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) took against a confidently-wrong match, applied to the instance rather than the dialect. A wrong answer delivered quietly costs more than a refusal. diff --git a/03-DESIGN/01-to-be/26-the-seats.md b/03-DESIGN/01-to-be/26-the-seats.md index d90dad0..cfb9a9d 100644 --- a/03-DESIGN/01-to-be/26-the-seats.md +++ b/03-DESIGN/01-to-be/26-the-seats.md @@ -1,6 +1,6 @@ --- layer: to-be -status: in-progress +status: proposed code: - mesh-controller internal/catalogue/seats.go - mesh-controller internal/catalogue/resolve.go @@ -17,8 +17,8 @@ decisions: # 26 — The seats -**What a mesh can have one of, and who fills each.** A seat is a named role at a scope, taken by a -module assignment. The mesh defines which seats exist. Occupying one may deliver a provision, and the +**What a mesh can have one of, and who fills each.** A seat is a named role at a scope, held by one +module assignment. The mesh defines which seats exist. Holding one may deliver a provision, and the list of seats with their holders is the quickest answer to "what is in this mesh". ## What a seat is @@ -27,28 +27,31 @@ A seat has four properties, fixed by the mesh rather than by any module: | property | is | |---|---| -| name | what a manifest claims, and what a person reads in the list | +| name | what a definition names and an assignment holds, and what a person reads in the list | | scope | node, site or mesh: where its capacity applies. Every seat in the set has a capacity of one, so one holder per scope. A bench, a seat with several holders, is a word the glossary keeps and no seat uses yet | | delivers | the provision its holder answers for, or nothing | | decision | the record that made it a seat | -**A module assignment holds a seat by claiming it.** The claim is the manifest's `claims`, and it is -satisfied by assigning the module somewhere. The seat is not a second record beside the assignment. -It points at the assignment, and everything the mesh knows about the holder is what it knows about -that assignment: the node, the node's settings for the module, and what the module serves. +**A definition says which seats a module can hold. An assignment says which it does hold.** The store +module can hold `mesh-store`, and it may run on every node whose capabilities match. Exactly one of +those assignments holds the seat, because that assignment says so, and a second assignment saying so +is refused. A seat makes a role singular, never a module. -**The set is closed.** A claim naming a seat the mesh does not define is refused, and so is a claim at -the wrong scope. Adding a seat is a decision, recorded, for the reason every addition to the host's -vocabulary is one: the set is what a person reads to learn what a mesh can have, and an entry nobody -argued for is an entry nobody can explain. +**The seat points at the assignment.** Everything the mesh knows about the holder is what it knows +about that assignment: the node, the node's settings for the module, and what the module serves. + +**The set is closed.** A seat the mesh does not define is refused wherever it is named, and so is one +named at the wrong scope. Adding a seat is a decision, recorded, for the reason every addition to the +host's vocabulary is one: the set is what a person reads to learn what a mesh can have, and an entry +nobody argued for is an entry nobody can explain. ## The set | seat | scope | delivers | typically held by | |---|---|---|---| | `mesh-controller` | mesh | — | the controller | -| `mesh-store` | mesh | — | the foundation's store | -| `mesh-broker` | mesh | `amqp` | the broker | +| `mesh-store` | mesh | — | the store the mesh's own records live in | +| `mesh-broker` | mesh | — | the broker carrying the mesh's own bus | | `mesh-vault` | mesh | `secret`, reserved | the vault | | `the-artifact-store` | mesh | `artifact-store` | the artifact registry | | `the-catalogue` | mesh | — | the catalogue | @@ -64,27 +67,24 @@ argued for is an entry nobody can explain. The controller holds this set in code, and a test asserts both its size and that every entry names the record that made it a seat. **This table and [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md) -govern, and code that disagrees is what is wrong.** The implementation in progress predates three -things here: the `mesh-vault` seat and its reservation, and the rule that `mesh-store` delivers -nothing. It is brought to this table before it merges. +govern, and code that disagrees is what is wrong.** The implementation in progress predates several +things here: seats held by assignments rather than claimed by definitions, the `mesh-vault` seat and +its reservation, and the foundation's seats delivering nothing. It is brought to this table before it +merges. + +## The foundation's seats + +`mesh-controller`, `mesh-store` and `mesh-broker` name which assignment the mesh *itself* uses: the +controller, the store holding its records, the broker carrying its bus. **They route no consumer.** The +store and broker modules may run on other nodes too. A database or `amqp` consumer is served by +co-location, from whichever runs on its own node, the seat's holder included +([23 — Choosing a provider](23-choosing-a-provider.md)). ## A seat that delivers a provision -A seat that delivers a provision may only be held by a module that provides it, at the seat's scope. -A mesh seat delivers a mesh-scoped provision. - -**A seat delivers a provision only where the mesh has one answer for everyone.** The broker, the -artifact store, the npm registry, git and the vault are each one per mesh by decision. The store is -not: nodes run their own stores and a consumer uses the one on its machine -([23 — Choosing a provider](23-choosing-a-provider.md)), and the foundation's store is the controller's -own memory, provider to nobody. So `mesh-store` guards that the foundation's store is singular, and -routes nobody. - -**The vault's provision is reserved.** Only the holder of `mesh-vault` may provide `secret` at all: a -module providing it without the seat is refused, and a pin cannot choose another provider, because -there is none. A second provider of secrets would be a second place secrets live, which is what the -vault being one per mesh exists to prevent. Every other delivered provision may have second -providers, which a pin can choose. +**A seat delivers a provision only where the mesh has one answer for everyone.** The artifact store, +the npm registry, git and the vault are each one per mesh by decision. A seat that delivers a +provision may only be held by an assignment of a module that provides it, at the seat's scope. **Its holder answers for that provision.** A requirement for it resolves, in order, to: @@ -96,21 +96,23 @@ providers, which a pin can choose. Co-location, which answers first for every other provision, does not apply here: a seat says which one is the mesh's, and co-location answering first would let any second provider on a consumer's machine take over for that consumer, silently. So a second provider can run beside the holder and -harm nothing. The forge holds -`npm-package-registry`. An npm proxy may provide the same provision on another machine, and a module -requiring an npm registry is still served by the forge, without anybody pinning it. +harm nothing. A forge assignment holds `npm-package-registry`. An npm proxy may provide the same +provision on another machine, and a module requiring an npm registry is still served by the forge, +without anybody pinning it. -**Moving the role is changing which module claims the seat, and today that is a definition change.** -A claim is part of a module's definition, so the proxy's definition must claim the seat and the -forge's must stop. The forge cannot simply be unassigned, because it holds `git` as well. Every -consumer follows once the claim moves. Making *which* seats a module holds the assignment's choice, -with the definition saying only which seats it *can* hold, is the consistent answer, and -[27 — A module requires, the mesh resolves](27-a-module-requires-the-mesh-resolves.md) lists it as -not yet settled. +**Moving the role is changing which assignment holds the seat.** No definition changes and nothing is +unassigned: the forge keeps running, and keeps holding `git`, when its npm role moves. A module can +take the role only if its definition says it can hold the seat. -**What a consumer receives is a grant**, the same as for any provision: where the provider answers, -what it serves, and a credential. A consumer never reads the seat directly. The one exception is the -controller itself, which reaches the store and the broker through a narrow seat placeholder, +**The vault's provision is reserved.** Only an assignment holding `mesh-vault` may provide `secret` at +all: a definition providing it that cannot hold the seat is refused, an assignment providing it without +holding the seat is refused, and a pin cannot choose another provider, because there is none. A second +provider of secrets would be a second place secrets live, which is what the vault being one per mesh +exists to prevent. + +**What a consumer receives is what it required**, the same as for any provision: where the provider +answers, what it serves, and a credential. A consumer never reads the seat directly. The one exception +is the controller itself, which reaches the store and the broker through a narrow seat placeholder, because it made them before any module existed and cannot be their consumer. One foundation module also reads it today, to find its own server's port. [27](27-a-module-requires-the-mesh-resolves.md) moves that to a host port requirement. @@ -123,9 +125,9 @@ their job, and it is a real one: it is the mesh saying what a machine is, in wor ## The overview -The controller lists every seat in the set with its scope, what it delivers, and each holder as a -node and a module. A seat nobody holds is listed as unheld. That is an answer, "this mesh has no -forge", and not a fault. +The controller lists every seat in the set with its scope, what it delivers, and its holder as a node +and a module. A seat nobody holds is listed as unheld. That is an answer, "this mesh has no forge", +and not a fault. Holdings are derived from assignments whenever they are asked for, never stored. The list is always what the mesh is running, because it is computed from the same thing that decides what the mesh runs. @@ -140,13 +142,13 @@ mesh records which: | on the `git` seat | a repository on the forge that holds the seat | its path on the forge, and the seat | | external | a repository anywhere else, a public forge for instance | its URL, exactly as given | -For a repository on the seat, the controller composes the clone URL at the moment of building, -from where the holder runs and the scheme and port it serves for `git`. The recorded source never -contains an address, so moving the forge changes nothing that was recorded. The build machine is not -told the difference: it receives a URL either way. +For a repository on the seat, the controller composes the clone URL at the moment of building, from +where the holder runs and the scheme and port it serves for `git`. The recorded source never contains +an address, so moving the forge changes nothing that was recorded. The build machine is not told the +difference: it receives a URL either way. With the seat unheld, a build from the seat is refused and says why. External builds carry on. **Not yet designed:** a credential for cloning a private repository. The mesh's own repositories are -public. The natural place for a clone credential is the `git` provision's grant, and that is a -decision still to take. +public. The natural place for a clone credential is a `secret` from the vault, and that is a decision +still to take. diff --git a/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md b/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md index a76658c..ca648af 100644 --- a/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md +++ b/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md @@ -96,9 +96,17 @@ per consumer, named for that consumer: Every other shared secret takes the same path: - a module's own secret; -- every broker account's password, where the broker's own provisioner creates the account; -- an enrolment token; -- a secret operator value, which the operator delivers to the vault. +- every broker account's password on the mesh's bus, where the broker's own provisioner creates the + account; +- an enrolment token, which the operator receives and the controller can only verify; +- a secret operator value, which the operator delivers to the vault; +- a secret a backend issues itself, such as a forge's API token, which the module that received it + delivers to the vault. + +**Parties that are not modules take the same path too.** The controller's own store login and bus +account, and each node agent's bus account, have no definition to require them, because the controller +and a node agent are the mesh itself. The controller asks the vault on its own behalf or a node's, and +the answer is made, sealed and carried exactly as for a module. **A provider makes resources and data.** Beyond secrets, a provider's adapter may answer with its contract's non-secret fields: an analytics site id, a registered public name. The mesh carries them @@ -116,7 +124,7 @@ lost is a named volume, not a directory ([ADR 0030](../../02-DECISIONS/0030-data *Where* a directory is on the machine is the assignment's: -- **a node's default layout**, a root per node with one directory per instance beneath it, used when +- **a node's default layout**, a root per node with one directory per assignment beneath it, used when the assignment says nothing; - **a placement**, where the assignment puts one directory elsewhere: on a second disk, or where an adopted machine's data already is ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)). @@ -175,35 +183,39 @@ module uses the seat placeholder. decided. The one exception 0086 allows is a declared env-file with its reason; a secret field as a value in a container's environment is refused when the definition is parsed, with no exception. -## An instance +## An assignment -An assignment has an identity: an **instance name**, which defaults to the module's name. Everything -keyed by the module's name today is keyed by the instance: directories, containers, the login it -presents, its broker account, the seats it holds, its settings and its identity as a provider. +**A module is assigned at most once to a node**, and that pair is the assignment's identity +([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md)). Its directories, +containers, login, broker account and settings are keyed by it, as today, and a login still fits the +tightest backend ([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)). -So a module may run twice on one node, under two instance names. What must stay singular stays so: -by a seat, or by an operator value colliding, as with a public name. +**A module may run on many nodes, and one assignment may hold a seat** +([ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)). The definition +says which seats the module can hold; the assignment says which it does. So the store module can run +on every node, one of those assignments holds `mesh-store`, and moving that role changes an +assignment, not a definition. -**A login still has to fit the tightest backend**, which is twenty characters today -([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)). A node name and -an instance name will not fit in full. The instance therefore gets a short form alongside the module's -slug, under the same rules as a slug. This has to be settled before a second instance is allowed. +What must stay singular stays so: by a seat, or by an operator value colliding, as with a public name. ## Genesis **Genesis delivers, and the vault adopts.** The vault cannot run first: it is built on the shared runtime base, which the installation makes only after the store, the broker and the controller exist ([21 — The installation in full](21-the-installation-in-full.md)), and it learns what to answer from -the controller over the bus. So genesis generates the foundation's first shared secrets itself: -- the store's superuser; -- the broker's admin, in the hashed form the broker needs; -- the bus accounts of the temporary controller and of the vault; -- the first enrolment token. +the controller over the bus. So the vault is installed **as soon as that base exists**, before any other +module built on it, and genesis generates what is needed until then: +- the store's superuser, and the broker's admin in the hashed form the broker needs; +- the bus accounts of the temporary and permanent controller, the control-node's agent, the builder, + the broker's own provisioner and the vault; +- the controller's store login, and the first enrolment token. -It seals them to the operator key as today ([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md)). -When the vault is installed, genesis **delivers them to it**, through the same path an operator's value -takes. From then on the vault holds, audits and rotates them. It can make their replacements, unlike -an operator's external key. +Until the broker's provisioner runs, genesis creates those bus accounts with the broker's admin, as the +controller does today; the provisioner adopts them when it starts. Genesis seals everything to the +operator key as today ([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md)), and when the +vault is installed it **delivers the values to it, recorded as the mesh's own**. That distinction keeps +them rotatable: an operator's value is never replaced, and these are, because the vault can make their +replacements. That is the one time anything but the vault generates a shared secret, and it ends by handing them over. It is also the answer to the objection ADR 0085 had to the vault being the only maker: the @@ -235,10 +247,16 @@ only when it first initialises is marked applied, because a restart would change recipient that reads at start has restarted and passed its health check, where its definition declares one. Delivered and working are shown as different things. -**The remaining window is stated.** An applier whose provisioner stops after applying and before -confirming leaves the recipients that read at start locked out: the old value no longer works, and -they have not been sent the new one. A provisioner is supervised and restarted when it exits, so the -window lasts until that restart. The rotation shows as waiting on that applier throughout, never as done. +**Open: keeping readers from being locked out.** Steps 2 and 3 leave a reader locked out when its +machine is offline after an applier applied, when the secret is a bus account whose owner loses the bus +it would hear the new value on, or when a restarted provisioner can no longer check the old value. +[ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md) records the two answers, overlapping +old and new credentials or re-confirming with safeguards, and one is chosen before it is accepted. +Neither changes a consumer module. + +A secret some service reads only when it first initialises cannot be rotated by restarting it. It is +applied by a provisioner, or, where none exists, marked not rotatable by the mesh, and a rotation is +refused rather than reported done. ## Refusing @@ -258,10 +276,11 @@ as waiting, and nothing is delivered until the answer arrives. | mechanism | becomes | |---|---| | provisions read through bindings | a module requirement; its answer is the contract's fields | -| settings on an assignment ([ADR 0046](../../02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md)) | operator requirements, addressed to an instance | +| settings on an assignment ([ADR 0046](../../02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md)) | operator requirements on an assignment | | a port the mesh assigns | a host requirement | | machine facts and machine placeholders | host requirements | -| every secret the controller mints: provider credentials, own secrets, broker passwords, root secrets | a secret the vault makes ([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)) | +| every secret the controller mints: provider credentials, own secrets, broker passwords, enrolment tokens | a secret the vault makes ([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)) | +| root secrets genesis mints and keeps apart | made by genesis once, then delivered to the vault, which holds and rotates them | | a separate command issuing a broker account | a requirement resolved on assignment | | `restart-on` naming a secret's file | a restart the host derives | | paths in resources, mounts, bindings, secrets and received files | host directory requirements, placed by the assignment | @@ -287,11 +306,11 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r is fixed first. *Ends when* nothing outside the vault generates a shared secret after genesis, a lab consumer of analytics receives its site id, and a database credential rotates applier-first, with the consumer restarted by derivation and the rotation confirmed. -3. **Definitions move.** Every catalogue definition is rewritten, adopted and running assignments - placed where their data already is. *Ends when* the list of definitions using an old form is - empty, and the old forms are removed. -4. **Instances.** The instance name and its short form, and everything keyed by it. *Ends when* one - module runs twice on one lab machine with two public names. +3. **Definitions move, and seats move to assignments.** Every catalogue definition is rewritten, adopted + and running assignments placed where their data already is, and each claim becomes a seat the + module can hold, held by the assignment that holds it today. *Ends when* the list of definitions + using an old form is empty, the old forms are removed, and the store module runs on two lab + machines with one holding `mesh-store`. ## How it is checked @@ -305,8 +324,9 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r | A host requirement is answered on its own node | A resolution test placing one elsewhere: refused. | | An operator value needs no provider module | A resolution test: a requirement with a default resolves with no module assigned anywhere. | | A secret field reaches a process as a file | The parser refuses a secret field as a container environment value, and accepts it in a file or a declared env-file with its reason. | -| A public name already held is refused | A resolution test: a second instance asking for a public name the first holds is refused, naming the first. | -| Everything keyed by a module is keyed by its instance | A resolution test: two instances of one module on one node get two directories, two containers, two logins and separate settings. | +| A public name already held is refused | A resolution test: a second assignment asking for a public name another holds is refused, naming the holder. | +| A module is assigned at most once to a node | A resolution test: assigning a module to a node that already runs it is refused. | +| A seat is held by an assignment, not a module | A resolution test: the store module on two nodes, one holding `mesh-store`; a second assignment asking to hold it is refused. | | A consumer waits for its provider | A resolution test with a provider that has not answered: shown as waiting, and nothing delivered. | | Only the vault generates a shared secret after genesis | A controller test: no code path generates one. An installer test: genesis generates exactly the foundation's first secrets and delivers them to the vault. | | Only the vault provides `secret` | The parser refuses another provider of it, and resolution refuses a pin on a `secret` requirement. | @@ -318,11 +338,8 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r ## Not settled here - The exact spelling of the one form. It must name a requirement and a field and nothing else. -- The layout a node's default root uses beneath it, beyond one directory per instance. +- The layout a node's default root uses beneath it, beyond one directory per assignment. - Whether a module provider's answer can change without the provider being asked, for example a provider moving. The rule so far is that it cannot, and moving is re-resolving. -- **Which seats a module holds.** Today a claim is part of the definition, so moving a seat is a - definition change ([26 — The seats](26-the-seats.md)). By this document's own logic it belongs to - the assignment: a definition says which seats a module *can* hold, and the assignment says which it - *does*. That changes [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md), - and is its own decision. +- **How rotation keeps a recipient from being locked out.** Under review: see + [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), rotation.