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