To-be 27 and ADR 0113 (proposed): a module requires, the mesh resolves

The design pass. Everything a module needs is a requirement: a name, a contract, and one of four kinds
of provider — a module, the node's host, the mesh, the operator. Installing a module resolves every
requirement or refuses, naming everything missing at once. It retires six mechanisms that grew
separately: provisions through bindings, settings, assigned ports, machine facts, minted secrets and
literals in the definition.

ADR 0113, proposed: a provider makes what it provides, and the mesh carries it back sealed to the
consumer's node. It is the return path ADR 0048 left "to a separate decision", now needed three ways:
data provisions with nothing to answer with, contracts needing a value the controller cannot make, and
a vault that generates nothing. Who a consumer is stays the mesh's (ADR 0049). Genesis is the one
exception. On acceptance it supersedes 0048 and amends 0085.

ADR 0112 is revised from three sources to that single concept.

ADR 0110 is amended for two points raised in review. The vault gets the mesh-vault seat (issue 106).
A seat's holder outranks co-location for a provision it delivers. Writing that down exposed an
inconsistency: mesh-store delivering postgres-database would have sent every database consumer to the
control-node, against to-be 23's node-local stores. So a seat delivers a provision only where the mesh
has one answer for everyone — artifact store, npm registry, git, vault — and mesh-store and mesh-broker
deliver nothing. 23 and 26 follow.

'Control plane' becomes 'controller' in the records written today.
This commit is contained in:
jochen
2026-09-25 22:34:46 +02:00
parent 7668190154
commit aad92ea8fe
9 changed files with 524 additions and 123 deletions
@@ -16,7 +16,7 @@ singular, at a scope, and a second holder is refused. [ADR 0079](0079-the-founda
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.
**Any well-formed name becomes a seat by being claimed.** The control plane's manifest check
**Any well-formed name becomes a seat by being claimed.** The controller's manifest check
refuses a claim only for a malformed name or an unknown scope. Nothing says which seats a mesh has.
The names in use were each invented by the module that claims them: `the-showcase`,
`the-build-machine`, `the-intrusion-prevention`.
@@ -24,9 +24,9 @@ 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 core modules' manifests moved into the control
plane's own repository ([ADR 0069](0069-a-module-is-a-repository-and-a-path.md)), and then the
control plane's code, because one module it ships has its manifest composed there. While this
every manifest in two repositories, because the core modules' manifests moved into the controller's
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.
@@ -68,34 +68,49 @@ second thing to disagree with the first.
**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. When several providers answer a requirement for that provision, resolution takes, in
order:
not is refused. 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, 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;
3. the **only** provider, when there is one;
4. otherwise, refused with the candidates named, as now.
2. **the holder of the seat** that delivers it, **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
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.
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.
**Seats are also informational.** The control plane lists every seat in the set, what it delivers,
**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 one.** Thirteen claims are in use, and
**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 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 and the
broker are 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. So `mesh-store` and `mesh-broker`
keep guarding that the foundation's own server is singular, and deliver nothing. Were they to
deliver, every database consumer on every node would be sent to the control-node's store.
**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 control plane claim today, so no module is refused
by it:
record admits every seat the catalogue and the controller claim today, so no module is refused by
it:
| seat | scope | delivers | held today 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-database` | `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-store` | mesh | — | `postgres` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
| `mesh-broker` | mesh | — | `lavinmq` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
| `mesh-vault` | mesh | `secret` | nothing yet: `mesh-vault` claims it | 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) |
@@ -103,15 +118,21 @@ by it:
| `the-dns-port` | node | — | `dnsmasq` | this record |
| `the-intrusion-prevention` | node | — | `fail2ban` | this record |
| `the-packet-filter` | node | — | `nftables` | this record |
| `the-private-network` | node | — | the control plane's private-network module | this record |
| `the-private-network` | node | — | the controller's private-network module | this record |
| `the-resolver-configuration` | node | — | `resolv-conf` or `resolved-split-dns` | this record |
| `the-showcase` | node | — | `showcase` | this record |
`npm-package-registry` is the one addition. It is [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md)'s
There are two additions. `npm-package-registry` is [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md)'s
seat, named after the provision it delivers, as 0079 named the foundation's seats after what they are.
gitea claims it. verdaccio provides the same provision and claims nothing, so it is the second
provider this record exists to make harmless.
`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.
`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
@@ -119,7 +140,7 @@ question real.
## Consequences
- The control plane carries the set in code. A test asserts its size, and that every entry names the
- The controller carries the set in code. A test asserts its size, and that every entry names the
record that made it a seat, so changing the set means finding the argument rather than a number.
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
@@ -133,7 +154,7 @@ question real.
`npm-package-registry` throughout, per [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md).
- **What got harder:** a module wanting a new singular role can no longer invent a name. It needs a
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 control plane can reach a
- **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.
@@ -12,7 +12,7 @@ extends: 0069-a-module-is-a-repository-and-a-path.md
## Context
[ADR 0069](0069-a-module-is-a-repository-and-a-path.md) made a module a repository, a path and a
ref, and the control plane records all three against the module so it can rebuild it and say when
ref, and the controller records all three against the module so it can rebuild it and say when
its source has moved ahead. **The repository is recorded exactly as a person typed it.** `build
<repository>` hands the string to a build machine, which runs `git clone` on it, and the same string
becomes the module's recorded source.
@@ -52,7 +52,7 @@ serving how a repository on it is cloned: the scheme and the port. gitea claims
- `build --self <owner>/<repository>` builds from a repository on the seat's holder. The recorded
source is the repository's path on that holder, and the seat it is on. **It never contains an
address.** At the moment of building, the control plane composes the clone URL from where the
address.** At the moment of building, the controller composes the clone URL from where the
holder runs and what it serves for `git`, so a moved forge changes nothing recorded.
- `build <url>` is unchanged: an external repository, recorded and cloned exactly as given. GitHub
and GitLab are the ordinary cases.
@@ -63,11 +63,11 @@ mesh without a forge of its own builds from external repositories only, and says
failing to clone.
**The build machine is not told the difference.** It receives a URL either way. Composing the URL is
the control plane's job, because only the control plane knows where the seat's holder runs.
the controller's job, because only the controller knows where the seat's holder runs.
## Consequences
- The control plane's inventory gains a column saying which seat a source is on. It is empty for
- The controller's inventory gains a column saying which seat a source is on. It is empty for
every module recorded before this, which is correct: they were all recorded as literal URLs.
- `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
@@ -7,7 +7,7 @@ reconstructed: false
extends: 0046-a-module-configuration-is-its-assignments-not-its-manifest.md
---
# 112. A module definition names no node, no mesh and no path: everything it needs is resolved at assignment
# 112. A module definition names no node, no mesh and no path: everything it needs is a requirement the mesh resolves
## Context
@@ -24,12 +24,20 @@ copies. The issue records what that has already allowed:
- defaults in code that disagree with their own manifests;
- no way to assign one module to one node twice, because every identity is keyed by the module's name.
**The mesh has already decided this once, for ports.** [ADR 0038](0038-the-mesh-assigns-the-port.md):
the mesh assigns the machine-side port and the module says only what it needs, and three copies
became one fact. [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md)
made the manifest identity and defaults, and the assignment's settings the configuration.
[ADR 0084](0084-which-provider-serves-a-consumer.md) made which provider serves a consumer part of
the assignment. Paths are the largest thing still left in the definition.
**Paths are one case of a wider pattern.** A module gets what it needs through at least six separate
mechanisms today, each with its own syntax and its own failure modes:
- provisions, read through bindings;
- settings on the assignment ([ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md));
- ports the mesh assigns ([ADR 0038](0038-the-mesh-assigns-the-port.md));
- machine facts a manifest asks for;
- secrets, either minted or accepted from an operator;
- literals carried in the definition itself.
The mesh has already unified parts of this. Ports became the mesh's rather than the module's (0038),
configuration became the assignment's (0046), and which provider serves a consumer became the
assignment's choice ([ADR 0084](0084-which-provider-serves-a-consumer.md)). What remains is the
concept that joins them.
## Considered Options
@@ -37,114 +45,123 @@ the assignment. Paths are the largest thing still left in the definition.
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.
**2. Keep host paths in definitions, and have the mesh rewrite them per assignment.** Rejected. It is
string surgery on paths: deciding which strings are machine paths by their shape, which is the
inference this repository has refused elsewhere. And the definition would still read as though it
decided where things live.
**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,
so a module author learns six systems and a reviewer checks six kinds of gap.
**3. A definition names variables, and the assignment resolves them.** Chosen.
**3. One concept: a module requires, and the mesh resolves every requirement against a contract.**
Chosen.
## Decision
**A module definition is node-agnostic and mesh-agnostic.** It names no node, no mesh and no host
path. Everything that makes a running instance *this* instance is a variable.
path. **Everything a module needs is a requirement**: a name, a contract saying what the module may
read from it, and which kind of provider answers it.
**Installing a module on a node resolves every variable, or refuses.** A refusal names each
unresolved variable and what could answer it. Variables are answered from three sources:
**Installing a module on a node resolves every requirement, or refuses.** A refusal names each
unresolved requirement and what could answer it, all at once.
1. **The assignment's own configuration.** Values chosen for this module on this node, carried as
settings ([ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md)). An
endpoint binding a public name to a port is one.
2. **Provisions, resolved by the mesh against a contract.** What other modules provide: a database,
a bucket, a vhost, a secret. Each comes with the contract the mesh and the module's
specification define. Where a provision has several providers, which one answers is part of the
assignment ([ADR 0084](0084-which-provider-serves-a-consumer.md)), so a module may take its
database from another node. Some have one provider per mesh by decision: a module's own secret
is a `secret` provision the controller mints and the vault records
([ADR 0085](0085-a-secret-is-a-provision.md), as amended). Whether the vault should instead
generate a secret against its contract is an open question, raised while reviewing this, and not
decided here.
3. **What the mesh generates or knows.** The credential the controller mints for each provision a
module takes ([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)), the ports it
assigns ([ADR 0038](0038-the-mesh-assigns-the-port.md)), facts about the machine.
**There are four kinds of provider, and the set is closed:**
**A directory is a provision, provided by the node's host.** A module requires one by name, such as
its configuration or its data. Its contract is the owner and mode it needs, including the owner its
image expects ([ADR 0107](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)). It
carries no persistence flag. A directory is kept while it holds anything
| provider | answers | today's mechanism it replaces |
|---|---|---|
| **another module** | a database, a bucket, a vhost, a secret, a route | provisions and bindings |
| **the node's host** | a directory, a port, facts about the machine | resource paths, `${port:}`, `${machine:}`, facts |
| **the mesh** | the module's identity and names, and the delivery of every answer | derived logins, minted delivery |
| **the operator, through the assignment** | a value a person chooses: a public name, a greeting, an external key | settings, carried literals |
A module provider is chosen as [ADR 0084](0084-which-provider-serves-a-consumer.md) and
[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) say: a pin, then the holder of a
seat that delivers the provision, then co-location, then the only one. A host provider is always the
module's own node, because a host path or a port means nothing on any other. An operator value is the
assignment's, or the requirement's default, or unresolved.
**A person's value stays cheap.** An operator requirement's contract is a type and, optionally, a
default. It needs no provider module, no grant and no credential. An operator value that is secret,
like an external API key, is kept by the vault as an operator-delivered value
([ADR 0092](0092-an-operator-delivers-a-pair-credential.md)).
**What a provider answers with is its contract's fields**, made by the provider and carried back to
the consumer by the mesh ([ADR 0113](0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)).
**A directory is a host provision.** Its contract is the owner and mode the module needs, including
the owner its image expects ([ADR 0107](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)).
It carries no persistence flag. A directory is kept while it holds anything
([ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md), which refused a `keep` flag for good
reason), and data that is disposable is not a directory at all but a named volume (0107).
*Where* a directory is on the machine is the assignment's. A node has a default layout, and an
assignment may place one directory elsewhere: on a second disk, or where an adopted machine's data
already is ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)). A directory is
always provided on the module's own node, because a host path means nothing on any other.
reason), and data that is disposable is not a directory but a named volume (0107). *Where* it is on
the machine is the assignment's. A node has a default layout, and an assignment may place a directory
elsewhere: on a second disk, or where an adopted machine's data already is
([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
**An operator's shared data stays an `access`** ([ADR 0051](0051-shared-data-is-the-operators.md)),
not a directory provision. 0051 rejected giving a directory an operator owner, because the mesh
must never create, chown or remove such data, and that stands. What changes is only where its
location is written: the module says it needs read or read-write access, and the assignment says
where the data is.
not a directory. 0051 rejected giving a directory an operator owner, because the mesh must never
create, chown or remove such data, and that stands. Only where its location is written changes: the
module requires read or read-write access, and the assignment says where the data is.
**Inside a container, a module sees its own paths.** The definition says where the image expects
each directory. The mesh mounts the assignment's location there. No host path is ever a value a
process reads.
**The mesh's own files carry no host path.** Bindings, secrets and contributions are named relative
to where the module receives them, so a provider reads what it was given without mounting anything
at a machine-identical path.
**Inside a container, a module sees its own paths.** The definition says where the image expects each
directory. The mesh mounts the assignment's location there. No host path is ever a value a process
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 the assignment's own configuration
colliding: a public name already taken is refused like any other singular thing.
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.
**Genesis is the one exception**, as [ADR 0113](0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)
states: the foundation's requirements are answered by genesis itself, before any provider exists.
## 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.
- [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 variable,
- [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 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md): settings are
addressed to an instance, not to a module on a node.
- [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.
- The [glossary](../00-META/glossary.md): *provision* widens from "a service one module provides" to
include a directory the node's host provides, and *instance* is added. Neither lands while this
- 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.
## Consequences
- **Every definition changes.** 70 of 71 name host paths today. The change is mechanical for most.
The design has to say how existing modules migrate without their data moving: an adopted or
already-running assignment is placed where its data already is.
- The controller resolves variables at assignment and refuses unresolved ones. The host provides
directories. The contributions file's format changes, and so does the SDK's reconcile loop that
reads it.
- **Every definition changes.** 70 of 71 name host paths today, and most use at least three of the
mechanisms this replaces. The change is mechanical for most. The design has to say how existing
modules move without their data moving: an adopted or already-running assignment is placed where
its data already is.
- 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.
- **What got harder:** a definition no longer says where a module's data is on a machine. 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 variable syntax; a node's default layout; whether a second instance of a
module is supported from the first step or after the definitions have moved; whether the vault
generates secrets.
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.
- **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.
## How it is checked
| Rule | Checked by |
|---|---|
| A definition names no host path | A catalogue test: the host side of every mount, and every resource location, binding, secret, receives and grants entry, is a variable rather than an absolute path. A declared list of exceptions shrinks to empty as definitions move. |
| A definition names no host path | A catalogue test: the host side of every mount, and every resource location, is a requirement rather than an absolute path. A declared list of exceptions shrinks to empty as definitions move. |
| No host path is a value a process reads | A catalogue test: every absolute path in a container's environment or env-files lies on the container side of one of its mounts, or is declared the image's own. A second test finds literal paths in module code used as fallbacks for an environment variable. |
| A definition names no node and no mesh | The parser has no field that names a node; a node is named only in an assignment. A catalogue test finds no domain name in any definition value. |
| Installation resolves every variable | A resolution test with one variable unanswered: refused, naming the variable and its possible sources. |
| A directory is provided on its module's own node | A resolution test: an assignment placing a directory on another node is refused. |
| A provider reads what it was given without an identical mount | A provisioner test reading a contributions file whose credentials are named relative to where it is mounted. |
| 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. |
| 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. |
@@ -152,10 +169,10 @@ On acceptance, each of these is amended by a record of its own, not edited:
## References
- [Issue 118](../04-ISSUES/118-a-module-definition-decides-where-its-files-live/00-report.md): the evidence
- [ADR 0038](0038-the-mesh-assigns-the-port.md): the same decision, for ports
- [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md): configuration is the assignment's
- [ADR 0084](0084-which-provider-serves-a-consumer.md): which provider answers is the assignment's
- [ADR 0085](0085-a-secret-is-a-provision.md), [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md):
secrets as a provision, and who mints what
- [ADR 0038](0038-the-mesh-assigns-the-port.md), [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md),
[ADR 0084](0084-which-provider-serves-a-consumer.md): the parts already unified
- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md): which module provider answers
- [ADR 0113](0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md): who makes an answer, and how it travels
- [ADR 0092](0092-an-operator-delivers-a-pair-credential.md): the operator as a provider
- [ADR 0051](0051-shared-data-is-the-operators.md), [ADR 0107](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md),
[ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md): what a directory's contract carries, and what it must not
@@ -0,0 +1,125 @@
---
topic: what runs on it
status: proposed
date: 2026-09-25
deciders: jochen
reconstructed: false
---
# 113. A provider makes what it provides, and the mesh carries it back to the consumer
## Context
[ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) decided that a provider is
handed the credential and makes none: the controller mints one per consumer and provider pair, seals
it to both nodes, and the provider creates the login under it. It fixed a real fault. The provider
harness of the time generated its own password and sealed it with a symmetric key nobody held, so a
consumer could never receive what the provider made. Controller-minting worked because it needed no
way back from provider to consumer.
**It left that way back undecided, deliberately.** 0048 says so: *"delivering provider-generated data
back to a consumer is a return path the mesh does not have and this decision does not build — a
separate shape, left to a separate decision."* Since then, the missing return path has come up
repeatedly:
- **Data provisions have nothing to answer with.** The analytics provider assigns a site id the
consumer needs. The DNS provider registers a name the consumer should be told. Both have no path
back, and say so in their code.
- **Some contracts need a value the controller cannot make.** The broker needs its admin password in
a hashed form the mesh's plain secret delivery cannot produce, so a module-specific bootstrap step
was written to derive it. A provider making the value to its own contract would not need one.
- **The vault is a ledger.** A module's own secret is "a `secret` provision the controller mints and
the vault records" ([ADR 0085](0085-a-secret-is-a-provision.md), as amended). The one module whose
job is secrets generates none, and cannot apply a policy (length, form, lifetime) because it never
makes one. Every other provider creates what it provides. The vault is the exception.
[ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) proposes that everything a module
needs is a requirement answered by a provider against a contract. Under that, "the controller mints
this one kind of answer on the provider's behalf" is a special case the model would carry forever.
## Considered Options
**1. Keep 0048: the controller mints credentials, and data provisions stay without a way back.**
Rejected. The vault stays a ledger, contracts needing a derived form keep needing bespoke steps, and
a data provision stays unable to answer at all.
**2. The provider makes the value and hands it to the consumer itself.** Rejected. This is the fault
0048 fixed. The provider cannot seal to the consumer's node, the two may be on different machines,
and a key both ends hold is the distribution problem one level down.
**3. The provider makes the value and gives it to the mesh, and the mesh carries it to the consumer.**
Chosen. The provider answers over its own scoped account. The controller, which already seals to
every node, seals each secret field to the consumer's node and delivers it the way it delivers
everything else.
## Decision
**A provider makes what it provides.** Given a consumer, it creates the resource and answers with
the fields its contract names: a password, an access key, a site id, a registered name, a hashed
admin secret. The vault generates the secrets it provides, to their contract, and rotates them.
**The mesh carries the answer back.** The provider hands its answer to the controller over its own
scoped broker account. The controller seals every field the contract marks secret to the consumer's
node, and delivers the answer as the consumer's resolved values. A provider never reaches a consumer
directly. Plaintext exists on the provider's machine, as it does today, and on the consumer's, and
nowhere between.
**Who a consumer is stays the mesh's.** The login a consumer presents is the mesh's derivation, which
both ends agree on by construction ([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md),
issue 023). A provider makes what a consumer is *given*, never what it is *called*.
**Rotation is the provider's act.** Asked to rotate, a provider makes a new value and answers again,
and the mesh redelivers it. An operator-delivered value ([ADR 0092](0092-an-operator-delivers-a-pair-credential.md))
is still never replaced by the mesh: its provider is the operator.
**Genesis is the one exception.** The foundation's own credentials and the vault's own access exist
before any provider can answer. Genesis mints those itself, seals them to the operator key as today,
and hands them to their holders. Nothing else is minted by the controller.
## What this changes in earlier records
On acceptance, each of these is superseded or amended by this record, not edited:
- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) is superseded: a provider no
longer receives a minted credential. Its refusal of provider-held symmetric keys stands, and is why
option 2 is rejected here.
- [ADR 0085](0085-a-secret-is-a-provision.md) is amended: the vault generates a module's own secret
rather than recording one the controller minted. "The vault stores no plaintext, ever" stands. It
makes a value, hands it to the mesh and keeps only what it keeps today.
## Consequences
- **A consumer waits for its provider.** Its requirement is not resolved until the provider has
answered, so resolution gains a state, *waiting on a provider*, that is shown rather than silent.
Today a consumer can receive a credential before the resource behind it exists. After this it
cannot.
- The provider harness in the SDK changes: an adapter's create answers with its contract's fields
instead of returning nothing, and every provider in the catalogue moves to it. This is one
migration per provider, the cost 0048 named for changing the contract, and it is paid once.
- The controller gains the return path: receiving an answer on a provider's account, sealing its
secret fields, and delivering them. Data provisions gain the same path, so the analytics and DNS
providers can finally answer.
- Bespoke derivation steps, like the broker's admin-hash bootstrap, become the provider's own
answer and can be removed.
- **What got harder:** a provider that is down cannot hand out credentials, where today the controller
could mint one in its absence. That is honest, because a credential for a resource that does not
exist yet was never usable, but it moves a failure from later and silent to earlier and visible.
## How it is checked
| Rule | Checked by |
|---|---|
| A provider's answer reaches the consumer sealed to its node | A controller test delivering a provider's answer: each secret field is sealed to the consumer's node key and to nothing else. |
| A consumer's identity is still the mesh's | A resolution test: the login a consumer presents is the mesh's derivation, whatever the provider answers. |
| A consumer waits for its provider | A resolution test with no answer yet: the requirement shows as waiting on a provider, and nothing is delivered. |
| The controller mints nothing outside genesis | A controller test: outside genesis, no code path mints a credential. The minting function is reachable only from genesis. |
| The vault generates and rotates | A vault test: a requested secret is generated to its contract, and a rotation answers with a new value. |
## References
- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md): the decision this supersedes,
and the return path it left open
- [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md): identity stays the mesh's
- [ADR 0085](0085-a-secret-is-a-provision.md), [ADR 0092](0092-an-operator-delivers-a-pair-credential.md):
the vault, and the operator as a provider
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md): everything a module needs is a requirement
+2 -1
View File
@@ -157,7 +157,8 @@ python3 00-META/checks/index.py fail if stale
- **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)
- **0112** — [A module definition names no node, no mesh and no path: everything it needs is resolved at assignment](0112-a-module-definition-names-no-node-mesh-or-path.md) *(proposed)*
- **0112** — [A module definition names no node, no mesh and no path: everything it needs is a requirement the mesh resolves](0112-a-module-definition-names-no-node-mesh-or-path.md) *(proposed)*
- **0113** — [A provider makes what it provides, and the mesh carries it back to the consumer](0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md) *(proposed)*
### How it is built