Files
hq/02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
T
jochen e387c4bd0e Apply review: two credentials, staged admin rotation, a ninth provider
The fact-check found mailu, whose user is its mailbox, so 0114 rotates
over two credentials rather than two logins, the adapter choosing what a
credential is. Also: minio keeps non-empty buckets; five backends take
their admin credential only at first init, so single-party rotation is
staged; postgres ownership moves to a non-login role; the harness keys by
consumer; rotation state lives with the vault. Consistency fixes across
0110-0113, 26 and 27; issue 103 resolved by mesh-host PR #22.
2026-09-26 00:38:06 +02:00

13 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
what runs on it proposed 2026-09-25 jochen false 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 a requirement the mesh resolves

Context

Issue 119 found 789 host-path strings in 70 of the catalogue's 71 module definitions. Each definition chooses where on the machine its directories, mounts, bindings, secrets, env-files and received files live, and often repeats that path in an environment variable or in code. Mounts are checked against what the definition declares (ADR 0091); nothing checks the copies. The issue records what that has already allowed:

  • a provider that would provision nobody without a word;
  • a contributions file that carries host paths into containers, so every provider must mount its grants directory at the identical path;
  • defaults in code that disagree with their own manifests;
  • every identity keyed by the module's name, which is why one module cannot be assigned to one node twice. This record keeps that, and says so below.

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);
  • ports the mesh assigns (ADR 0038);
  • 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). What remains is the concept that joins them.

Considered Options

1. Keep host paths in definitions, and check that every copy agrees. Rejected. It checks the agreement of something that should not be there. A definition still could not follow its data to another disk, or be adopted onto a machine whose data is already somewhere.

2. Keep the separate mechanisms, and add directories as a seventh. Rejected. It fixes paths and keeps the pattern that produced them: each mechanism is resolved, validated and refused differently, so a module author learns six systems and a reviewer checks six kinds of gap.

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 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 requirement, or refuses. A refusal names each unresolved requirement and what could answer it, all at once.

There are four kinds of provider, and the set is closed:

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 and generated names; the controller's delivery
the operator, through the assignment a value a person chooses that is not secret: a public name, a greeting, a number of workers settings, carried literals

A module provider is chosen as ADR 0084 and ADR 0110 say. A requirement naming a seat is answered by its holder. Otherwise it is a pin, then co-location, then the only provider, and where several remain, a person chooses at assignment and the choice is recorded as a pin. 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.

Every shared secret is a secret requirement, answered by the vault, with no exception by kind (ADR 0113). A private key is made where it is used and is not a requirement. An external API key an operator chooses is no different: the operator delivers it to the vault (ADR 0092), and the module requires a secret like any other. A provider that needs a secret for a consumer requires it from the vault, like any consumer, and answers with resources and data. The mesh carries every answer back.

A directory is a host provision. Its contract is the owner and mode the module needs, including the owner its image expects (ADR 0107). It carries no persistence flag. A directory is kept while it holds anything (ADR 0030, which refused a keep flag for good 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).

An operator's shared data stays an access (ADR 0051), 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, and the mesh's own files (answers, contributions) name nothing by host path, so a provider needs no mount at a machine-identical path.

A module is assigned at most once to a node. An assignment is a module on a node, and that pair is its identity: its directories, containers, login, broker account and settings are keyed by it, as they are today. A module may run on many nodes, and one of those assignments may hold a seat (ADR 0110). Running the same module twice on one machine is not supported. The cases that seemed to need it, such as two stores of one engine or two stages of one application, are different modules, or the same module on different machines. The line is drawn because every identity in the mesh is already a module on a node, and a second instance would have to rename all of them.

What must stay singular stays so by a seat, or by an operator value colliding: a public name already held by another assignment is refused like any other singular thing.

The foundation's first secrets are delivered, then adopted. Genesis generates them before the vault can run and hands them to the vault once it is installed, and from then on they are answered the same way as every other secret (ADR 0113).

What this changes in earlier records

On acceptance, each of these is amended by this record, not edited:

  • ADR 0046: settings become operator requirements on an assignment.
  • ADR 0038: a port becomes a host requirement. What 0038 decided is unchanged; it is the first case of this rule.
  • ADR 0051: an access keeps its shape and its semantics; its path moves from the definition to the assignment.
  • ADR 0091: a mount's host side is a resolved requirement, checked as resolved rather than as a path the definition declares.
  • To-be 21: the step that builds and runs a store module as a database provider, beside the foundation's store on the same node, would run the store module twice on one node. The adopted store module (ADR 0078) holds mesh-store and serves that node's database consumers by co-location, so there is no second one.
  • The glossary: provision widens from "a service one module provides" to a requirement answered by any of the four providers, and requirement and contract are added. None of it lands while this record is only proposed, because the glossary is the authority on the words in use, not on words under review.

Consequences

  • 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 stays a module on a node. Nothing is renamed, and a login still fits the tightest backend as ADR 0049 arranges.
  • What got harder: one module cannot run twice on one machine; a second stage or a second store of one engine is a different module or a different machine. And a definition no longer says where a module's data is on a machine, or what a setting's value is. The assignment does, and plan shows it. That is the point, and it is also a real loss of at-a-glance legibility, which the overview has to give back.
  • Not decided here: the syntax a definition reads a requirement's fields with; a node's default layout; the order in which the mechanisms are retired. To-be 27 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, 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.
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 is assigned at most once to a node A resolution test: assigning a module to a node that already runs it is refused, naming the existing assignment.
A public name already taken is refused A resolution test: a second assignment asking for a public name another holds is refused, naming the holder.
An adopted assignment is placed where its data is An adoption test: the directory resolves to the data's existing location, and nothing is moved.

References