Files
hq/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
jschoubben ce6ae943b7 Merge main: renumber this branch's records around the trunk's
Both lines of work numbered from the same point, so four decision records and one design
document existed twice with different content. The trunk keeps its numbers and this branch
yields — the only rule that scales, because the trunk's are already cited by what merged
before them.

  0117 the bus is the only broker        -> 0125
  0118 a module declares its own seats   -> 0126
  0119 amqp is a provision, not the bus  -> 0127
  0120 the mesh bus is required          -> 0128
  0123 a seat carries its role's protocol -> 0129
  0124 the predecessor is ending          -> 0130
  design 29, what a module declares       -> design 32

Applied to the code repositories too, because a stale reference is worse when numbers
collide than when they dangle: the reader lands on a real record that decided something
else.

Two reconciliations the merge forced, both real:

**0110 was marked wholly superseded and was not.** Its successor says in as many words that
everything 0110 decided about what a seat *is* stands untouched — and two records that
landed on the trunk rest on exactly that part. So it is accepted again, extended rather than
replaced, with a note saying which of its claims moved and where.

**A seat's protocol becomes columns, not fields.** The trunk moved the seat set out of
compiled code into a table the controller owns. This branch had added what a role accepts,
emits and serves to the Go slice. The decision is unaffected and the mechanism is better for
it: giving a role a protocol is now a write rather than a rebuild, which is the trunk's own
argument applied to what this branch added.

One check still fails and it fails on main too: a record resting on ADR 0112 while that is
still 'proposed'. Left alone — it is not this merge's to answer.
2026-09-27 18:23:41 +02:00

27 KiB

layer, status, code, updated, decisions
layer status code updated decisions
to-be proposed
2026-09-26
02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
02-DECISIONS/0115-one-assignment-of-a-module-per-node.md
02-DECISIONS/0113-the-vault-makes-every-secret.md
02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md
02-DECISIONS/0126-a-module-declares-its-own-seats.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, issue 119).

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. the holder of the seat the requirement names. A requirement may name a seat instead of leaving the provider open. It asks for the mesh's one, and the mesh answers with whichever assignment holds that seat, with nothing asked of anyone. Unheld, the requirement is refused, naming the seat (ADR 0126 (superseding ADR 0110), 26 — The seats). Only a seat that delivers a provision can be named; naming a foundation seat is refused, because it delivers nothing. A secret requirement always names mesh-vault, because that provision is reserved;
  2. a pin: the assignment names a provider, because this consumer is coupled to that provider's contents (ADR 0084);
  3. the provider on the consumer's own node;
  4. the only provider in the mesh;
  5. otherwise a person chooses, at assignment. Assigning the module lists the candidates, with the holder of a seat that delivers the provision suggested first, and the answer is recorded on the assignment as its pin. Without an answer the module is not assigned, and the refusal names the candidates. Nothing is ever guessed (ADR 0009).

Secrets: provisioning all the way down

Two kinds of secret, one rule each (ADR 0113):

  • 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).

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.

The controller takes the same path, because it is a module. Its store logins and bus accounts are own secrets of its definition today, and become requirements of that definition. A node's host is the one party with no definition, because it is what runs definitions. Its bus account is a requirement the mesh makes for each enrolled node, answered 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, ADR 0107).

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).

An operator's shared data is an access, as before (ADR 0051): 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 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).

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), 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 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). Its directories, containers, login, broker account and settings are keyed by it, as today, and a login still fits the tightest backend (ADR 0049).

A module may run on many nodes, and one assignment may hold a seat (ADR 0126 (superseding ADR 0110)). 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), 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 (its account and the broker-management login), the control-node's host, the builder, the broker's own provisioner and the vault;
  • the controller's three store logins, 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 control-node's key, and when the vault is installed the controller delivers the values to it, recorded as the mesh's own, with nobody present. 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.

Raising the vault or the broker again is a genesis act. Moving either seat to a new assignment, or recovering either after it is lost, delivers the values it needs the way genesis did. It is a stated break-glass procedure, and an ordinary assignment attempting it is refused.

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 requirement's contract, and the vault makes the new value (ADR 0113). An operator's external key is not rotated by the vault, which cannot make its replacement: an operator delivers a new one.

Each requirement says how its recipient takes a new value. It either applies it, through a provisioner (a provider setting a login's password, the broker's provisioner updating an account, a store's provisioner changing its own superuser), or reads it at start. Every module in the catalogue reads its secrets at start, and none watches them. The host already recreates a container when a file it read at creation changes, its env-files and files mounted into it directly (issue 103). So a reader's restart is derived, and a definition declares restart-on only for a secret reaching a process, or a file in a mounted directory. A secret a service takes only at first initialisation is applied by its provisioner or marked not rotatable by the mesh, and a rotation of it is refused rather than reported done.

How old and new change over depends on how many parties hold the credential (ADR 0114, on research 016):

  • Two parties, a consumer and its provider, or a module or node's host and the broker: each consumer has two credentials, both with its rights over one resource named after the consumer. For most providers the second is a second login derived by the mesh; where a backend's user is its resource, it is a second token. The vault drives the rotation and records each step. It makes the new value; each applier ensures the unused credential and confirms both authenticate; only then are readers given it and recreated; each reader confirms by authenticating with it; and only then is the old one retired. Nobody is left without a credential that works, a rotation can be abandoned until the old one is retired, and status shows who a rotation waits on.
  • One party, a provider's administrative credential or a module's own secret: in place, staged. An applied one is delivered beside the current value, the provisioner changes the backend with the current one, and only then does the new value become current. One read at start is delivered, and the host recreates the module.

Retiring a credential never removes what it reached. An adapter keeps retire a credential and remove the consumer apart, and the harness keys what it applied by consumer, so a changed login is never a removal. The resource is removed only when the consumer no longer requires it from that provider: unassigned, the requirement dropped, or re-resolved elsewhere. Today these are one call, and in five providers it deletes the consumer's data, so this separation comes first. An adapter that cannot yet ensure a second credential rotates in place, with its window stated, and is listed until it can.

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;
  • several candidates and no choice made, and which they are;
  • 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) 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)
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 is fixed in the host already. 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 over its two credentials, the consumer recreated by derivation, never without a working login, and its data intact.
  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.
A module provider is chosen by named seat, pin, co-location, only one, a person's choice Resolution tests for each step: a requirement naming a seat served by its holder even with another provider on the consumer's node, and refused when the seat is unheld; several candidates and none local, where assignment lists them with the seat's holder first and records the choice as a pin, and refuses without one.
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.
Restarts are derived The tests of ADR 0113: an applied secret restarts nothing, and one read at start recreates its reader without a declared restart.
A two-party credential rotates over two credentials The rotation tests of ADR 0114: retiring a credential leaves the resource intact; a changed login is never a removal; readers move only after the applier confirms and confirm by authenticating; an unreachable reader keeps its old credential until it returns; rotation state survives a restart; a single-party applied secret is staged.
A private key is made where it is used The per-key tests of ADR 0113: a node's sealing key, the operator's key and the certificate authority's key never leave where they were made.
The controller and a node's host take the same path 0113's tests: the controller's definition declares requirements and no own secret; a node's bus account is made by the vault and delivered sealed to that node.
Moving the vault or the broker is break-glass A resolution test: an ordinary assignment moving mesh-vault or mesh-broker is refused, naming the procedure.
A secret that cannot be rotated says so A vault test: rotating a secret marked not rotatable by the mesh is refused, naming why.
Only the controller reads the seat placeholder A catalogue test, from phase 3: no definition uses the seat placeholder.
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.

The three gaps, answered (2026-09-26, operator)

Where the root comes from. A node setting, fixed at installation; a node that states none gets the default under /var/lib. (The adopted-machine placement stays as proposed: a per-assignment placement for data that must sit where it already is — mssql is novox's live case.)

What sits beneath it — dissolved, not decided. The question assumed the mesh's own writes (/var/lib/mesh/<module>: sealed credentials, composed bindings) need a module-visible reservation. They do not: a module requires a host-path and receives a location; what the mesh writes for the module is the mesh's plumbing, placed where the mesh chooses and mounted in — never part of the module's contract. One reservation per requirement, <root>/<module>/<name>.

Resolution happens in the controller, at declaration composition. The node receives concrete paths exactly as today — the wire format and the host's apply do not change for this. What changes is that no manifest carries a path; the controller resolves requirement → location against the node's root setting. (The host still needs issue 126's fix — volumes in the spec comparison — or a resolved path change cannot reach a running container.)

Retirement order stays per-module data migration with verification, smallest and empties first, the mail spool and the store last — the novox session's six-module window (mesh-catalog #97, hq 126) is the worked example, hazard included.