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:
@@ -51,12 +51,15 @@ 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.
|
||||
|
||||
**A seat names the mesh's one provider of a kind.** Where a seat delivers the provision, its holder
|
||||
answers for it when several providers exist and the consumer named none. That is not picking: the
|
||||
choice was made once, mesh-wide, by assigning the holder, rather than once per consumer by naming it
|
||||
**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.
|
||||
coupled to particular contents has said so. Only provisions the design makes one-per-mesh are
|
||||
delivered by a seat: 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
|
||||
|
||||
@@ -47,8 +47,9 @@ argued for is an entry nobody can explain.
|
||||
| seat | scope | delivers | typically held by |
|
||||
|---|---|---|---|
|
||||
| `mesh-controller` | mesh | — | the controller |
|
||||
| `mesh-store` | mesh | `postgres-database` | the store |
|
||||
| `mesh-broker` | mesh | `amqp` | the broker |
|
||||
| `mesh-store` | mesh | — | the foundation's store |
|
||||
| `mesh-broker` | mesh | — | the foundation's broker |
|
||||
| `mesh-vault` | mesh | `secret` | the vault |
|
||||
| `the-artifact-store` | mesh | `artifact-store` | the artifact registry |
|
||||
| `the-catalogue` | mesh | — | the catalogue |
|
||||
| `npm-package-registry` | mesh | `npm-package-registry` | the forge |
|
||||
@@ -61,7 +62,7 @@ argued for is an entry nobody can explain.
|
||||
| `the-resolver-configuration` | node | — | whichever of the alternative resolver configurations is chosen |
|
||||
| `the-showcase` | node | — | the showcase module |
|
||||
|
||||
The control plane holds this set in code, and a test asserts both its size and that every entry names
|
||||
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 document follows the code, not the reverse. If the two disagree,
|
||||
the test has been changed without this table, and the table is what is wrong.
|
||||
|
||||
@@ -70,16 +71,23 @@ the test has been changed without this table, and the table is what is wrong.
|
||||
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.
|
||||
|
||||
**Its holder answers for that provision.** When a requirement for it has more than one provider in the
|
||||
mesh, the control plane takes, in order:
|
||||
**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. The store and the broker are
|
||||
not: nodes run their own stores and a consumer uses the one on its machine
|
||||
([23 — Choosing a provider](23-choosing-a-provider.md)). So their seats guard that the foundation's
|
||||
own server is singular, and route nobody.
|
||||
|
||||
**Its holder answers for that provision.** A requirement for it resolves, in order, to:
|
||||
|
||||
1. the provider the consumer's node was pinned to, because a consumer coupled to one provider's
|
||||
contents has said so ([23 — Choosing a provider](23-choosing-a-provider.md));
|
||||
2. the holder of the seat;
|
||||
3. the only provider, when there is one;
|
||||
4. otherwise nothing, and the requirement is refused with the candidates named.
|
||||
2. the holder of the seat, **even when another provider runs on the consumer's own machine**;
|
||||
3. otherwise nothing, and the requirement is refused, naming the unheld seat.
|
||||
|
||||
So a second provider can run beside the holder and harm nothing. The forge holds
|
||||
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. Moving the role
|
||||
to the proxy is moving the seat: unassign the claim from one, assign it to the other, and every
|
||||
@@ -87,7 +95,7 @@ consumer follows.
|
||||
|
||||
**What a consumer receives is a grant**, the same as for any provision: where the provider answers,
|
||||
what it serves, and a credential where one is minted. A consumer never reads the seat directly. The
|
||||
one exception is the control plane itself, which reaches the store and the broker through a narrow
|
||||
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.
|
||||
|
||||
## A seat that delivers nothing
|
||||
@@ -98,7 +106,7 @@ their job, and it is a real one: it is the mesh saying what a machine is, in wor
|
||||
|
||||
## The overview
|
||||
|
||||
The control plane lists every seat in the set with its scope, what it delivers, and each holder as a
|
||||
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.
|
||||
|
||||
@@ -115,7 +123,7 @@ 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 control plane composes the clone URL at the moment of building,
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
---
|
||||
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-a-provider-makes-what-it-provides-and-the-mesh-carries-it.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 118](../../04-ISSUES/118-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 mesh knows each contract, and a provider is checked against the one 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 | the controller | derived logins, generated names |
|
||||
| **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. **the provider on the consumer's own node**, for a provision no seat delivers;
|
||||
4. **the only provider** in the mesh;
|
||||
5. otherwise **refused**, naming the candidates, or the unheld seat.
|
||||
|
||||
The provider makes what it provides and answers with its contract's fields. The mesh carries the
|
||||
answer back to the consumer, sealing every secret field to the consumer's node
|
||||
([ADR 0113](../../02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)). The vault
|
||||
is a module provider like any other: it holds the `mesh-vault` seat and generates the secrets it
|
||||
provides.
|
||||
|
||||
### 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 instance 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, is held by the vault as an
|
||||
operator-delivered value ([ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md)),
|
||||
never stored as a setting.
|
||||
|
||||
**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, machine facts and seats. The seat placeholder the controller uses to reach its own foundation
|
||||
stays, because the controller cannot be a consumer of a foundation it made before any module
|
||||
existed. It is the controller's own and no module uses it.
|
||||
|
||||
**A secret field reaches a process as a file**, as today ([ADR 0086](../../02-DECISIONS/0086-a-secret-reaches-a-process-as-a-file.md)).
|
||||
Reading one into a plain value, such as an environment variable, is refused when the definition is
|
||||
parsed, unless the definition declares the exception 0086 allows, with its reason.
|
||||
|
||||
## An instance
|
||||
|
||||
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.
|
||||
|
||||
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 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.
|
||||
|
||||
## Genesis
|
||||
|
||||
The one exception. Before any provider exists, genesis answers the foundation's own requirements
|
||||
itself: the store's and broker's credentials, the vault's own access, and the root secrets. It seals
|
||||
them to the operator key as it does now ([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md)).
|
||||
After genesis, nothing is answered except by a provider.
|
||||
|
||||
## 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 that has not answered yet, and which one.
|
||||
|
||||
The last one is a state, not a failure. A consumer waiting for its provider is shown as waiting, and
|
||||
nothing is delivered until the answer arrives ([ADR 0113](../../02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)).
|
||||
|
||||
## 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, addressed to an instance |
|
||||
| a port the mesh assigns | a host requirement |
|
||||
| machine facts and machine placeholders | host requirements |
|
||||
| a secret the controller mints | a secret the vault makes ([ADR 0113](../../02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)) |
|
||||
| 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. **Resolution and the new form.** 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. **Providers answer.** The SDK harness answers with contract fields, the controller carries answers
|
||||
back and seals them, and the vault holds its seat and generates. *Ends when* the analytics and DNS
|
||||
providers answer their consumers and the broker's bootstrap step is removed.
|
||||
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.
|
||||
|
||||
## 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, and for a second provider on a consumer's own machine when a seat delivers the provision. |
|
||||
| A host requirement is answered on its own node | A resolution test placing one elsewhere: refused. |
|
||||
| An operator value needs no provider | 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 read into a plain value, unless the definition declares the 0086 exception with a reason. |
|
||||
| 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 instance.
|
||||
- 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.
|
||||
- A contract registry: where contracts live, and how a new provision gets one. Today contracts are
|
||||
implicit in each provider's served fields.
|
||||
@@ -35,6 +35,7 @@ document is written and this one's status becomes `implemented`.
|
||||
| [`23-choosing-a-provider.md`](23-choosing-a-provider.md) | Which of several providers of a kind serves a consumer, and when a module carries its own instead | [ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md), [ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) |
|
||||
| [`24-the-secrets-vault.md`](24-the-secrets-vault.md) | The module that owns a secret — a `secret` provision, and the boundary of what it owns | [ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), [ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md), [ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md) |
|
||||
| [`26-the-seats.md`](26-the-seats.md) | What a mesh can have one of, who fills each, and a seat's holder answering for the provision it delivers — including the `git` seat a build's source can live on | [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md), [ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md), [ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md) |
|
||||
| [`27-a-module-requires-the-mesh-resolves.md`](27-a-module-requires-the-mesh-resolves.md) | **Proposed.** One concept for everything a module needs: a requirement with a contract, answered by one of four kinds of provider, resolved at assignment or refused. Retires settings, placeholders, facts and paths in definitions | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0113](../../02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md), [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md) |
|
||||
|
||||
## Not yet written
|
||||
|
||||
|
||||
Reference in New Issue
Block a user