--- topic: what runs on it status: proposed date: 2026-10-01 deciders: jochen reconstructed: false extends: 02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md --- # 164. A setting is declared with its default, its meaning and what changing it costs ## Context The operator asked for one thing for every module, with the container runtime as the first case: **one consistent default configuration for every machine, overridable per assignment, and easy to change later.** The four machines' runtime configurations were each written by hand and differ — one keeps running containers through a daemon restart and one does not, their log rotation differs, and each names its resolver and its trusted registries in its own words. Most of this was already decided. [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) said the definition is identity and **defaults**, the assignment's settings are the configuration, *unset is the default*, and *an unknown setting is refused*. [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) made a setting an operator requirement whose contract is "a type and, optionally, a default", answered by "the assignment's, or the requirement's default, or unresolved". An assignment is a module on a node, so the node layer of a module's settings already *is* the per-assignment override, and the mesh-wide layer is the one consistent default a person changes once. What was built is narrower than what was decided, measured in the controller on the day of deciding: - **Nothing declares which keys are settable.** A mergeable file's content is its defaults, and every key of it — and every key not in it — is accepted. Nothing tells a person, or the console, what can be set, of what type, or what it means. - **The refusal of an unknown setting is not there for most modules.** The stray-setting report returns nothing at all for a module with any mergeable file, because such a file "takes any key" ([issue 173](../04-ISSUES/173-a-modules-settings-reach-every-fact-it-contributes/00-report.md) left files that way on purpose). It reports rather than refuses where it does run. - **A value in a file that is not JSON can have no default.** `${setting:}` ([ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md)) is refused when no layer sets it — right for a mail domain, where a default is the very literal 0155 removes, and wrong for a tunable like the resolver's upstreams, which the resolver module therefore carries as literals in its file. - **What a change costs is said per file, not per key.** A service names the files it is reloaded or restarted on. The runtime re-reads its trusted registries on a reload and its `dns` key only when it starts; the resolver module declared a reload, so on two machines the key was written, reloaded, and never read, and every container got a public resolver for weeks while everything read as current ([issue 110](../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/01-resolution.md)). ## Considered Options 1. **Leave settings implicit; document each module's keys in its README.** Rejected: a key the mesh does not know cannot be refused, typed, listed by the console or costed, and a README is a rule enforced by nothing. 2. **A second mechanism for tunables beside settings** — defaults in a new block, settings untouched. Rejected: two ways to state one person's value, and design 27 already retires six mechanisms that grew that way. 3. **Settings declared in the definition, as 0112's operator requirement: a key, a type, a meaning, optionally a default, and what a change costs.** Adopted. ## Decision **A module declares every setting it takes.** Each declared setting has a name, a type, one sentence of meaning, optionally a default, and what a change to it costs. The spelling is design 27's to settle with the rest of the requirement form; this record decides the content. **A setting with a default is a tunable; a setting without one is the operator's.** A tunable resolves to its default when no layer sets it — wherever it is read, a mergeable file or `${setting:}` in a file of any format. A setting with no default is refused by name when nothing sets it, as 0155 decided; 0155's refusal is narrowed to exactly that case, not changed for it. Whether a value has a default is a fact about the software (a log size does, a mail domain does not), and the definition states it once. **The layers stay as they are, and every value says where it came from.** The definition's default, then the mesh-wide layer, then the node's — later wins, objects merge, lists replace. One consistent configuration for every machine is the default plus the mesh-wide layer; one machine that differs says so in its own layer and nothing else. Asked for a module's configuration on a machine, the mesh lists every declared setting with its effective value and its source: *default*, *mesh*, or *node*. **Changing later is changing one of three places, and the plan shows its reach before anything moves.** A new default ships with the module's next version and reaches every assignment that does not override it; a mesh-wide setting reaches every assignment of the module; a node's reaches one. The plan of a change names each assignment whose effective value moves. **A declared setting is the only kind accepted.** Setting a key the module does not declare is refused when it is set, naming the declared keys, rather than reported when the machine is planned. A module that declares no settings keeps today's behaviour until it does; a catalogue test lists those modules, and the list shrinks to empty before the implicit form is removed — design 27's rule for every retired mechanism. **A setting says what it costs: nothing, a reload, or a restart.** When a file changes, the host applies the strongest cost among the settings whose values moved in it, so a key the software reads only at start can no longer be written and never read. A service's `reload-on` and `restart-on` keep naming the files that are not settings — a generated roster, a credential. **The container runtime is the first module to declare its settings** and the model for the rest: its log rotation, keeping containers through a daemon restart, and its resolver are tunables, and its trusted registries are what the mesh tells it. ## Consequences - The console can show a module's settings as a form: what can be set, of what type, its default, and where the current value came from. That is the surface the operator wants for changing a default later. - `settings set` can refuse an unknown key, so ADR 0046's rule is enforced where it was only stated. - The resolver's upstreams, the runtime's log rotation, and other literals a definition carries because it could not give them a default become declared tunables. - **What got harder:** every module that takes settings must list them, and a mergeable file no longer silently accepts a key its author did not foresee. A person who needs one adds it to the definition, which is a new module version, not a setting. - Issue 173's open question — a consumer checks nothing against a contract — is unchanged; this record is the operator half of design 27's contract, not the provider half. - Not decided here: the spelling (design 27); whether a node's layer may be narrowed to a single key rather than replaced whole, as `settings set` does today. ## How this is checked | Rule | Checked by | |---|---| | Every setting a module takes is declared | A parser test refusing a setting declaration without a type or meaning; a catalogue test listing modules with mergeable files or `${setting:}` and no declarations, which must be empty before the implicit form is removed | | A tunable resolves to its default; an operator value without one is refused | Resolution tests: an unset tunable in a JSON file and in a text file both take the default; an unset setting with no default is refused naming it (0155's existing test) | | An undeclared key is refused when set | A controller test: `settings set` with an undeclared key fails naming the declared keys, and nothing is stored | | Every effective value names its source | A test listing a module's configuration on a node with one key from each of default, mesh and node | | A change's reach is shown before it moves | A plan test: changing a mesh-wide setting names every assignment whose effective value moves and no other | | The strongest cost applies | A host test: a file where a reload-cost key and a restart-cost key both moved restarts; a file where only reload-cost keys moved reloads | | Live | The container runtime's module lists its settings with their sources on every machine, and a mesh-wide change to its log rotation reaches all four at the next push | ## References - [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md), [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md) - [Design 27 — a module requires, the mesh resolves](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md) - Issues [110](../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md), [173](../04-ISSUES/173-a-modules-settings-reach-every-fact-it-contributes/00-report.md) - mesh-controller `internal/catalogue/settings.go` (`settle`, `UnusedSettings`), `internal/catalogue/setting_into.go`