Files
hq/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
T
jochen 4a1b218706 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.
2026-09-25 23:47:26 +02:00

346 lines
23 KiB
Markdown

---
layer: to-be
status: proposed
code: []
updated: 2026-09-25
decisions:
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
- 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
- 02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md
- 02-DECISIONS/0038-the-mesh-assigns-the-port.md
---
# 27 — A module requires, the mesh resolves
**One concept for everything a module needs.** A module definition states what it requires. Each
requirement has a contract and a kind of provider. Installing the module on a node resolves every
requirement, or refuses and says why. Nothing else reaches a module: no path it chose, no setting
beside the model, no literal it carries.
This replaces six mechanisms that grew separately: provisions read through bindings, settings,
assigned ports, machine facts, minted or accepted secrets, and literals in the definition. Each
resolved, validated and failed in its own way ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md),
[issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md)).
## A requirement
A requirement has three parts:
| part | is |
|---|---|
| name | what the module calls it, unique within the module |
| contract | the fields the module may read, and what each promises: a type, whether it is secret, and anything the provider must honour |
| provider kind | which of the four kinds of provider answers it |
**A contract is shared, not per module.** A database's contract is the database's, whoever requires
it. **The controller holds every contract**, one per provision name, declared where the provision is
defined in the catalogue. Today contracts are implicit in each provider's served fields; the first
phase below makes them explicit, because nothing can be checked against a contract that is not
written down. A provider is checked against the contract it claims to answer. A module's own
specification may narrow a contract (a password of at least this length, a directory owned by this
user) and never widen it.
## The four kinds of provider
The set is closed, like the seats. A fifth kind is a decision, because each kind is a place an answer
can come from and a reviewer has to know every one.
| provider | answers | resolved by | replaces |
|---|---|---|---|
| **a module** | a database, a bucket, a vhost, a route, a secret | the rule below | provisions and bindings |
| **the node's host** | a directory, a port, a fact about the machine | always the module's own node | resource paths, assigned ports, machine placeholders, facts |
| **the mesh** | the module's identity and names, and the delivery of every answer | the controller | derived logins and generated names; the controller's delivery |
| **the operator** | a value a person chooses | the assignment, else the requirement's default | settings, carried literals |
### A module provider
Which module answers, in order:
1. **a pin**: the assignment names a provider, because this consumer is coupled to that provider's
contents ([ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md));
2. **the holder of a seat** that delivers the provision, where one does. Co-location does not apply
to these: the seat is the mesh's one answer for everyone ([ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md),
[26 — The seats](26-the-seats.md));
3. for a provision no seat delivers, **the provider on the consumer's own node**;
4. for a provision no seat delivers, **the only provider** in the mesh;
5. otherwise **refused**: naming the unheld seat, for a provision a seat delivers, even when exactly
one provider exists; naming the candidates otherwise.
### Secrets: provisioning all the way down
**Two kinds of secret, one rule each** ([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)):
- **a shared secret**, a value more than one party must hold (a password, a token, an API key), is
made by the vault, and by nothing else;
- **a private key**, such as a node's sealing key, the operator's key or the mesh's certificate
authority, is made where it is used and never leaves. A private key anyone else held would no
longer be private.
**Every shared secret is a `secret` requirement, and only the vault provides `secret`.** The vault
holds the `mesh-vault` seat, and that provision is reserved to it: no other module may provide it, and
no pin can choose another provider ([26 — The seats](26-the-seats.md)).
**A provider that needs a secret for a consumer requires one, like any consumer.** A provision's
contract declares it: *for each consumer, one secret*. Resolution expands that into one requirement
per consumer, named for that consumer:
1. gitea requires `postgres-database`;
2. the database provider, to serve gitea, requires a `secret` named for gitea;
3. the vault makes it and hands it to the mesh;
4. the mesh delivers it to both of its **recipients**, each sealed to its own node: the database's
machine, which *applies* it by creating the login, and gitea's, which *presents* it;
5. the database provider creates the login, exactly as it does today, and gitea connects.
Every other shared secret takes the same path:
- a module's own secret;
- 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
back to the consumer as resolved values.
### The node's host
The host answers what only a machine can: where a directory is, which port is free, what the machine
is. It is always the module's own node, because none of these means anything elsewhere.
**A directory.** The contract is an owner and a mode, and the owner the image expects where it has
one. There is no persistence flag. A directory is kept while it holds anything, and data that may be
lost is a named volume, not a directory ([ADR 0030](../../02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md),
[ADR 0107](../../02-DECISIONS/0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)).
*Where* a directory is on the machine is the assignment's:
- **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)).
**An operator's shared data** is an access, as before ([ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md)):
never created, owned or removed by the mesh. The module requires read or read-write access. Where
the data is, is an operator value on the assignment.
**A port** is what [ADR 0038](../../02-DECISIONS/0038-the-mesh-assigns-the-port.md) already decided: the
module says which port its software uses, and the host answers with where the machine put it.
**A fact** is something the machine knows: its name on the private network, the names of the mesh's
machines. Each fact has a contract like anything else.
### The mesh
The mesh answers who the module is: its login, its broker account, the names it is known by. These
are derived by the mesh so every party agrees by construction, and no provider may make them
([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)).
### The operator
A value a person chooses: a public name for an endpoint, a greeting, how many workers to run.
**It must stay cheap.** An operator requirement's contract is a type and, optionally, a default. It
needs no provider module, no grant and no credential. If asking a person for a value took more than
that, module authors would route around it, and the literals this replaces would come back.
**A secret operator value**, such as an external API key, follows the one rule for secrets: the
vault provides it. The operator hands the value to the vault, once
([ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md)), and the module requires
a `secret` like any other. The only difference is that rotation never replaces it: the vault cannot
make a new external key, so rotating one means an operator handing over a new value.
**An endpoint** is an operator value inside a route requirement: the public name is chosen on the
assignment, and the route provider answers. A public name already held by another assignment is
refused, like any other singular thing.
## How a definition reads what was resolved
**One form, naming a requirement and a field of its contract.** A definition that needs the database's
host in an environment variable, the directory's location on the host side of a mount, or the public
name in a configuration file writes the same thing: the requirement's name and the field. The
controller fills it at resolution.
This one form replaces the placeholders that exist today, one per mechanism: bound values, secrets,
ports and machine facts.
**The seat placeholder stays, for the controller alone.** The controller composes its own
declaration and reaches the store and broker it made before any module existed, so it cannot be
their consumer. One module reads the placeholder today: the store module, to find its own server's
port. That is its own port, so it becomes a host port requirement in phase 3, and after that no
module uses the seat placeholder.
**A secret field reaches a process as a file**, as [ADR 0086](../../02-DECISIONS/0086-a-secret-reaches-a-process-as-a-file.md)
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 assignment
**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)).
**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.
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 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.
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
vault cannot make what exists before it, so what exists before it is delivered to it.
## Rotation
Rotating a secret is asked of the vault, by an operator or by the vault's own policy, such as a
maximum age in the secret's contract ([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)).
An operator's external key is not rotated by the vault, which cannot make its replacement: an
operator delivers a new one.
**A secret's contract says how each recipient takes a new value.** A recipient either *applies* it,
through a provisioner (a provider creating the login, the broker's provisioner updating an account,
the store's own provisioner changing its superuser), or *reads it at start*. A secret a service reads
only when it first initialises is marked applied, because a restart would change nothing.
1. **The vault makes the new value.**
2. **It goes 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. It repeats that confirmation on every
reconcile pass until the vault acknowledges it, so a lost message costs one pass.
3. **Only then is it released to the recipients that read it at start**, such as gitea.
4. **The host restarts every such recipient**, and recreates a container whose env-file carries the
secret. It knows which, because a definition reads a secret only through its requirement, so no
definition declares a restart for a secret. An applying recipient is never restarted for it.
This needs [issue 103](../../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)
fixed, or a container fed by an env-file keeps the old value.
5. **It is confirmed.** The rotation shows as unconfirmed until every applier has confirmed, and every
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.
**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
Installation refuses when any requirement is unresolved, and **says everything at once**. For each
requirement it names what is missing and what would answer it:
- an unheld seat, and which modules could hold it;
- no provider, and which modules could provide it;
- an operator value with no default, and that the assignment must give it;
- a provider, or the vault, that has not answered yet, and which one.
The last one is a state, not a failure. A consumer waiting for its provider or for the vault is shown
as waiting, and nothing is delivered until the answer arrives.
## What this retires
| 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 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, 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 |
| literals carried in a definition | operator requirements with defaults |
Each is retired only once nothing uses it. Until then both are accepted, and a catalogue test lists
the definitions still using the old form. That list shrinks to empty, and then the old form is
removed from the parser.
## Phases
Each phase ends at a check that holds, so none of them leaves a mechanism half-replaced.
1. **Contracts, resolution and the new form.** Every provision's contract is written down and held by
the controller. The controller resolves requirements from the four providers, refuses as above,
and fills the one form. Old mechanisms keep working beside it. *Ends when* a definition written
entirely in the new form installs on a lab machine.
2. **The vault makes every shared secret, and providers answer.** The vault holds its seat and its
reserved provision; resolution expands per-consumer secret requirements; genesis delivers the
foundation's first secrets to the vault; the broker's provisioner creates every bus account; the
mesh carries providers' data back.
[Issue 103](../../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)
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, 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
| Rule | Checked by |
|---|---|
| Every requirement has one of the four provider kinds | The parser refuses any other. |
| A definition names no host path, node or mesh | The catalogue tests of [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md). |
| A module provider is chosen by pin, seat, co-location, only one, refusal | Resolution tests for each step; for a second provider on a consumer's own machine when a seat delivers the provision; and for an unheld seat with exactly one provider, refused. |
| A provider answers within its contract | A controller test: an answer carrying a field its contract does not name, or missing one it does, is refused and not delivered. |
| A module narrows a contract and never widens it | The parser refuses a module specification that loosens a contract's field. |
| 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 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. |
| A provider's per-consumer secret comes from the vault | A resolution test: requiring a database expands to a secret requirement named for the consumer, answered by the vault and delivered to both recipients. |
| Rotation is applier-first, derived and confirmed | The rotation tests of [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md): readers wait for every applier's repeated confirmation; an applied secret restarts nothing, and one read at start restarts its reader without a declared restart; the rotation shows unconfirmed until the new value authenticates, the old does not, and readers are healthy. |
| Refusal names everything at once | A resolution test with three unresolved requirements of different kinds: one refusal naming all three. |
| The old forms retire | The catalogue test listing definitions still using one. It must be empty before a form is removed. |
## 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 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.
- **How rotation keeps a recipient from being locked out.** Under review: see
[ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), rotation.