Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b69ae663bc | ||
|
|
0b9fc90885 |
+1
-1
@@ -108,7 +108,7 @@ term retired here may still appear there, and the mapping above is how to read i
|
||||
memberships issue; its serving mode on loopback is what was called **the console**
|
||||
([ADR 0175](../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
|
||||
Replaces **"console"** as the module's name; *console* remains the word for the person's end of it.
|
||||
- **bundle** — the artifact a module's own code is built into — its tools, a seat's implementation, a daemon — in any language the mesh has a toolchain for, interpreted or compiled; never an image. One module may declare several ([ADR 0188](../02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)).
|
||||
- **bundle** — the artifact a module's tools are built into, interpreted or compiled; never an image.
|
||||
- **kept region** — a marked block in a managed file the mesh writes *into*, where the operator's own
|
||||
lines survive every push and are given back when the module goes
|
||||
([ADR 0174](../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)).
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
status: graduated
|
||||
initiated: 2026-10-03
|
||||
touches: [the tool runtime, the catalogue's tool bundles, the controller's declaration composer, settings, own secrets, 03-DESIGN/01-to-be/38-building-the-operators-machine.md]
|
||||
became: [02-DECISIONS/0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md, 03-DESIGN/01-to-be/38-building-the-operators-machine.md]
|
||||
---
|
||||
|
||||
# 020 — What a bundled tool is given
|
||||
|
||||
## What is being investigated
|
||||
|
||||
How a module's tools, once they are a bundle the node's runtime loads
|
||||
([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md),
|
||||
[ADR 0188](../../02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)),
|
||||
learn the things their container used to be handed: where the module's configuration file is, where
|
||||
its token or password is, which port the service listens on, where a provision's address is written.
|
||||
A container is given these as an environment and mounts, composed by the mesh per module per machine.
|
||||
A bundle has no environment of its own: the runtime's process carries four words for every bundle it
|
||||
loads, and nothing per module ([design 38](../../03-DESIGN/01-to-be/38-building-the-operators-machine.md) WP4).
|
||||
|
||||
## Why
|
||||
|
||||
Two holders moved on 2026-10-03 — the packet filter and the intrusion prevention — and both could,
|
||||
because neither needs anything but a fixed path and root. Of the thirty-three modules whose tools
|
||||
still run as containers on the runtime's image, thirty-one are not like that: their environment
|
||||
names a configuration file, a credential file, a service address, a grants directory. Moving them
|
||||
one by one without a rule for this would give the mesh thirty-one answers to one question. The
|
||||
measurement and the options are in [01](01-what-the-containers-are-given.md).
|
||||
|
||||
## What it touches
|
||||
|
||||
The runtime (which hands a bundle what it is given), the composer (which resolves `${dir:…}` and
|
||||
`${port:…}` for a container today and would for a bundle), the manifest (where a bundle would say
|
||||
what it needs), and design 38, which records the gap and must say the rule once there is one.
|
||||
@@ -1,86 +0,0 @@
|
||||
# What the tool containers are given, measured
|
||||
|
||||
Counted 2026-10-03 in the catalogue, after the two holders moved.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| modules whose tools still run as a container on the runtime's image | 33 |
|
||||
| tool containers among them (two modules run two) | 36 |
|
||||
| modules whose container's environment carries only the bus credential | 1 (the intrusion prevention, now moved) |
|
||||
| modules whose container's environment carries more | 32 — 31 still containers |
|
||||
|
||||
## What "more" is
|
||||
|
||||
Every value a container is given is one of five shapes. The reference kinds the composer resolves
|
||||
in those values, over the 36 containers: a module directory (`${dir:…}`) in all 36, a mesh-chosen
|
||||
port (`${port:…}`) in 12, a seat and an access grant once each.
|
||||
|
||||
1. **A file the mesh already places on the host, mounted in.** The module's configuration as
|
||||
JSON (`…_CONFIG_FILE`), its own secret (`…_TOKEN_FILE`, `…_PASSWORD_FILE`, `MESH_BROKER_FILE`),
|
||||
a provision's address and secret written for it. Every one is a path under one of the module's
|
||||
directories — its mesh state, its state, its grants, what it has written — mounted at a path of
|
||||
the container's choosing and named to the tool through the environment. **The file is on the
|
||||
host already; only the name under which the tool finds it is the container's.**
|
||||
2. **The service's address, with the port the mesh chose:** `http://127.0.0.1:${port:3000}`. The
|
||||
port is the composer's; the rest is the manifest's constant.
|
||||
3. **A provision's address as a constant string** (a database's URL on the module's own network
|
||||
name), paired with a mounted secret file from shape 1.
|
||||
4. **A directory of grants** (`MESH_RECEIVES`): shape 1 again, a directory rather than a file.
|
||||
5. **Literals the image needs:** a time zone, a user id, a memory limit. These belong to the
|
||||
service's container where one exists; a tool bundle needs none of them.
|
||||
|
||||
So the whole of what a bundled tool needs is: the paths of its module's directories on this
|
||||
machine, the ports the mesh chose for its module here, and the constants its own manifest wrote.
|
||||
Nothing a container had that a bundle cannot have; the mesh composes all three for the container
|
||||
today, per module per machine.
|
||||
|
||||
## What the runtime already has for it
|
||||
|
||||
- The SDK's tool contributor is `(env) => tools`, and `collectTools(env)` takes the environment to
|
||||
hand each contributor. The runtime calls it without one, so every contributor reads the process's
|
||||
— the four words. The hook for a per-module environment exists and is unused.
|
||||
- A launched bundle ([ADR 0188](../../02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md))
|
||||
is spawned with the runtime's environment; the launch takes an environment argument.
|
||||
- The composer resolves `${dir:…}` and `${port:…}` for a container's `env` and `volumes`; the same
|
||||
resolution over a bundle's declaration is the same code.
|
||||
|
||||
## Options
|
||||
|
||||
**A. The bundle declares its environment on its artifact, and the mesh composes it as a
|
||||
container's.** The manifest's tools artifact gains `env`, resolved with the same references;
|
||||
values that were mount targets become the host-side paths directly (`${dir:mesh-state}/config.json`
|
||||
rather than `/run/config/config.json`). The controller composes one environment per bundle per
|
||||
machine into the runtime's declaration; the runtime hands it to the bundle's contributor and to a
|
||||
launched child, and to nothing else. *For:* the tool code does not change — it reads the same
|
||||
names; the conversion of the thirty-one is a mechanical move of the container's `env` with the
|
||||
mounts folded in; one rule, one place. *Against:* the runtime's process carries thirty-one
|
||||
environments in its declaration, and a bundle's environment is visible to the other bundles in the
|
||||
process unless the runtime keeps them apart, which it must — a tool that reads `process.env`
|
||||
instead of the environment it was handed would see its neighbours' paths.
|
||||
|
||||
**B. The runtime derives the environment from the module's placed manifest.** No new field: the
|
||||
runtime reads, for each module it serves, where that module's directories and ports are, and hands
|
||||
a conventional set of words. *For:* nothing to declare. *Against:* a convention the tool code must
|
||||
be rewritten to, thirty-one times; the runtime learns the composer's job; a module that names its
|
||||
file `config.json` and one that names it `settings.json` need different words anyway.
|
||||
|
||||
**C. Tools read their module's files through the bus** — ask the controller. *Against:* a tool
|
||||
that cannot start without the bus answering a question is a tool that fails in the one case the
|
||||
tools exist for, and a secret crossing the bus to reach a file already on the machine is a
|
||||
disclosure for nothing.
|
||||
|
||||
A is the one that keeps the tool code and the composer's vocabulary as they are, and names the one
|
||||
thing the runtime must add: an environment per bundle, kept apart. The thing to decide beside it:
|
||||
whether a bundle's environment may name a secret file at all, or whether secrets stay mounts in
|
||||
spirit — a path the tool reads, never a value in the environment — which is what every container
|
||||
does today and what A keeps if the rule says *paths, not values*.
|
||||
|
||||
## What a decision would have to say
|
||||
|
||||
- Where a bundle says what it is given (the artifact, option A), and that values are paths and
|
||||
constants, never a secret's content.
|
||||
- That the composer resolves it with the references it already has, per module per machine.
|
||||
- That the runtime hands each bundle its own environment and nothing of another's, and how that is
|
||||
checked: a test loading two bundles whose environments differ and asserting each sees only its own.
|
||||
- That the thirty-one move in one mechanical change after the rule lands, each proven by its tools
|
||||
answering from the runtime, and the registration gate then refuses the container shape for all.
|
||||
@@ -8,8 +8,6 @@ reconstructed: false
|
||||
|
||||
# 39. What the SDK holds, and what it refuses
|
||||
|
||||
> **The mechanism changed — 2026-10-02, by [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md).** The test of this record — frequent *and* cascading does not belong — stands and now applies to one SDK per language. Where *what it holds* names the broker client and the event consumer, read: the local protocol a tools bundle speaks to the node's runtime; the transport lives in the runtime and in no SDK, which is what keeps a bus change from rebuilding any module in any language.
|
||||
|
||||
_Reconciliation note (2026-09-05): supersedes the earlier "repository structure" decision, which the consolidation folded; no standalone record remains to point at, so body references to it now point at the nearest surviving record, [ADR 0015](0015-applications-live-in-their-own-repository.md)._
|
||||
|
||||
## Context
|
||||
|
||||
@@ -9,13 +9,6 @@ extends: 0007-connectivity.md
|
||||
|
||||
# 66. Public routing is name-agnostic, its names are resolved inside the mesh, and an internal authority can certify them
|
||||
|
||||
> **Narrowed, not replaced — 2026-10-03.** One clause of the decision below no longer holds: *publishing
|
||||
> a granted name into internal resolution, mesh-wide*. A public name now resolves publicly, and only
|
||||
> names under the mesh's own suffix get a private answer — [ADR 0191](0191-the-meshs-resolver-holds-only-the-meshs-own-names.md).
|
||||
> Inside the mesh a route is reached and certified by its internal name
|
||||
> ([ADR 0151](0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md)). The label, the
|
||||
> node's public domain and their composition stand as decided here.
|
||||
|
||||
## Context
|
||||
|
||||
**[ADR 0007](0007-connectivity.md) and [connectivity §3](../03-DESIGN/01-to-be/08-connectivity.md)
|
||||
|
||||
@@ -9,8 +9,6 @@ extends: 0110-a-seat-is-a-module-assignment-from-a-closed-set.md
|
||||
|
||||
# 121. A system seat is named for its scope, and a module may define its own
|
||||
|
||||
> **The mechanism changed — 2026-10-02, by [ADR 0190](0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md).** The naming rule stands. The build role this record made mesh-scoped — *the mesh's single build machine* — is node-scoped now: `node-build-agent`, one holder per machine, every holder taking from one work queue.
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) made seats a closed set the
|
||||
|
||||
@@ -9,8 +9,6 @@ extends: 02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-ow
|
||||
|
||||
# 150. A module's own code runs as supervised processes under the module's one account
|
||||
|
||||
> **Widened — 2026-10-02, by [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md).** A module's long-lived process is a bundle in any language the mesh has a toolchain for, run as a unit the host writes; this record never said one language and never meant one, and 0188 says so as the rule.
|
||||
|
||||
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** For a module's *tools*, read that record: one runtime per node, the node's one account, bundles loaded from the memberships. This record still governs a module's long-lived processes — a daemon, a provisioner, a scheduled ingest — and the account invariant for them.
|
||||
|
||||
## Context
|
||||
|
||||
@@ -9,11 +9,6 @@ extends: 02-DECISIONS/0066-public-routing-is-name-agnostic.md
|
||||
|
||||
# 151. A route's internal name is composed under the node that serves it
|
||||
|
||||
> **Narrowed, not replaced — 2026-10-03.** *"The roster publishes it as itself, once, at the serving
|
||||
> node's address"* no longer holds: a public name is never given a private answer, and resolves publicly
|
||||
> ([ADR 0191](0191-the-meshs-resolver-holds-only-the-meshs-own-names.md)). The internal name this record
|
||||
> composes is what that rests on, and stands.
|
||||
|
||||
## Context
|
||||
|
||||
A module that requires a route is given two names from one label: a public one, `<label>.<public
|
||||
|
||||
@@ -9,8 +9,6 @@ extends: 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||
|
||||
# 162. A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue
|
||||
|
||||
> **Progressive insight — 2026-10-02.** The context below says a dependent is *built by whichever build machine is running — the only one there could be*. That was a fact of the day, not of the decision: since [ADR 0190](0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md) a tier's asks are taken by every machine holding the build seat. The plan and its tiers are unchanged.
|
||||
|
||||
## Context
|
||||
|
||||
A merge on the forge reaches the controller as an event, and the controller asks the build
|
||||
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
---
|
||||
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:<key>}`
|
||||
([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.
|
||||
- **A setting reaches every mergeable file its module owns.** The layers are one flat map per module,
|
||||
laid over each such file. Adding a setting to the resolver module for its own configuration put the
|
||||
key into the container runtime's file as well — the resolver writes into that file too — and the
|
||||
runtime refuses keys it does not know. The plan showed it before any push; the runtime's file was
|
||||
then made to take no settings at all ([issue 198](../04-ISSUES/198-the-lans-dns-server-ran-outside-the-mesh-and-its-filter-closed-it/00-report.md)). Issue 173 stopped settings leaking into
|
||||
contributions and served facts; between one module's own files the leak remains.
|
||||
- **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:<key>}` 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 says where it lands.** Each names the file or files of its module that read it,
|
||||
and reaches no other: a module that owns two mergeable files no longer has one flat map laid over both.
|
||||
A file that names no setting takes none.
|
||||
|
||||
**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. The mesh's
|
||||
own words — where a port, a directory or an operator's data is placed, how far an endpoint reaches —
|
||||
are the mesh's to validate as they are today, and no module declares them. 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 setting that reaches a container's environment
|
||||
costs that container being recreated, which the host already does when a container's specification
|
||||
changes; it needs no declaration. 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) |
|
||||
| A setting reaches only the files it names | A resolution test: a module with two mergeable files and a setting declared for one; the other file's content is unchanged by it (the case of issue 198) |
|
||||
| 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`
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: proposed
|
||||
date: 2026-10-01
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0161-what-deserves-a-seat.md
|
||||
---
|
||||
|
||||
# 165. `container-runtime` is what a machine can run; that a runtime is running is its holder's health
|
||||
|
||||
## Context
|
||||
|
||||
A capability is a requirement a module places on a machine, detected by the host and renewed with
|
||||
every report ([ADR 0161](0161-what-deserves-a-seat.md)). The host's `container-runtime` asks the
|
||||
daemon for its version: *a running daemon, not an installed client*. It was made that way by
|
||||
[issue 007](../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md), where an
|
||||
installed package was believed to be a working service, and
|
||||
[design 05](../03-DESIGN/01-to-be/05-the-node-host.md)'s table says the same: *a runtime is
|
||||
running*. The installer's preflight borrows the same detector to wait for the runtime the
|
||||
foundation bundle installs, so there is one answer to "is there a runtime here".
|
||||
|
||||
The mesh is now to have a module for the runtime itself — its packages, its configuration, its
|
||||
service — on every machine ([ADR 0166](0166-the-container-runtime-is-a-node-seat-and-the-host-creates-containers-through-its-holder.md)).
|
||||
That module cannot declare `container-runtime` as defined: it would require the very thing it
|
||||
installs, the cycle [research 011](../01-RESEARCH/011-the-module-graph/cases.md)'s case 12 names
|
||||
("something the mesh installs that then becomes a node capability"). The operator defined the word
|
||||
for it: **`container-runtime` means the machine is able, at the kernel level, to install a runtime and
|
||||
execute containers** — not that one is installed, and not that one is running.
|
||||
|
||||
The host already draws this line once. `seat` is hardware, a display server *could* run here;
|
||||
`graphical-session` is state, one *is* running; the detector's own comment says "assignment needs the
|
||||
first". A machine without a display has no seat however much software is installed, and a machine
|
||||
with one has a seat before anything is.
|
||||
|
||||
Fifty-four catalogue modules declare `container-runtime` today, counted on the catalogue's main
|
||||
branch on the day of deciding: every module that delivers a container. Each relies on the current
|
||||
meaning to keep it off a machine with no running runtime.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Keep the meaning; let the runtime's module declare nothing.** Rejected: a module that installs
|
||||
the runtime has requirements on the machine — the kernel features without which installing it is
|
||||
pointless — and would state none of them. The cycle stays, only hidden.
|
||||
2. **Two capabilities, "can run" and "is running".** Rejected: the second is made true by assigning a
|
||||
module, so it is the module's state, not a fact of the machine; a capability the mesh itself
|
||||
flips by its own assignment is case 12's cycle with an extra name.
|
||||
3. **The capability is the kernel's; whether a runtime runs is the runtime module's health, and a
|
||||
module that delivers a container needs the runtime's seat held.** Adopted.
|
||||
|
||||
## Decision
|
||||
|
||||
**`container-runtime` is detected from what the kernel offers**, as `seat` is: the namespaces a
|
||||
container needs, a control-group hierarchy the runtime can manage, and an overlay filesystem the
|
||||
running kernel has or can load. Present when all three are; absent naming the missing one. Nothing is
|
||||
run and no runtime is asked. The verdict's detail names what was found, not a runtime's version.
|
||||
|
||||
**"A runtime is running and answers" is one probe, owned by the host and used twice:** by the
|
||||
installer's preflight, which waits for the runtime the foundation installs, and as the runtime
|
||||
module's health. It asks the daemon, as issue 007 requires. The preflight stops borrowing the
|
||||
capability's detector, and there is still one answer to "is a runtime running here".
|
||||
|
||||
**The runtime's module declares `container-runtime`**, with `package-manager`, `service-manager` and
|
||||
`privileged`, like any module that manages machine software.
|
||||
|
||||
**A module that delivers a container needs the runtime seat held on its machine**, and is refused
|
||||
otherwise, naming the seat and the modules that could hold it — the refusal design 27 already lists
|
||||
for an unheld seat. That requirement is derived from the container resource and needs no manifest
|
||||
field ([ADR 0166](0166-the-container-runtime-is-a-node-seat-and-the-host-creates-containers-through-its-holder.md)).
|
||||
The fifty-four existing declarations of the capability stay valid and become redundant; a catalogue
|
||||
test lists them, and they retire when the list is empty.
|
||||
|
||||
**The order is fixed, not preferred.** The detector changes only once the seat requirement is
|
||||
enforced. In between, a machine with the kernel and no running runtime would read as able to run
|
||||
every containerised module, which is issue 007 again.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Design 05's capability table changes its `container-runtime` row from *a runtime is running* to
|
||||
*the kernel can run containers*, and names the runtime module's health as where "running" is now
|
||||
asked.
|
||||
- The node listing stops showing the runtime's version beside the capability. The version moves to
|
||||
the runtime module's health and its seat's verbs.
|
||||
- A fresh machine with no runtime reads as able to run one, so it can be assigned the runtime's
|
||||
module, which is what makes the mesh able to install the runtime instead of the bootstrap alone.
|
||||
- **What got harder:** "is this machine running containers" is no longer one glance at the profile;
|
||||
it is the runtime seat's holder and its health. The node's listing should show both side by side.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| The capability is the kernel's | Host detector tests over a fixture `/proc` and `/sys`: all three present → present; each one missing → absent naming it; no runtime binary on the fixture machine changes nothing |
|
||||
| One probe asks whether a runtime runs | A host test that the preflight and the runtime module's health call the same probe, and that the probe fails against a stopped daemon with an installed client (issue 007's shape) |
|
||||
| A containerised module needs the runtime seat held | A resolution test: a module with a container resource on a machine whose runtime seat is unheld is refused, naming the seat and its candidate holders |
|
||||
| The order holds | The host release that changes the detector is gated on the controller release that enforces the seat requirement — stated in both changes' descriptions and checked at review |
|
||||
| Live | Every machine's profile shows `container-runtime` present with the kernel's features as its detail; a machine with no runtime installed reads present too |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0161](0161-what-deserves-a-seat.md) — the profile renewed by every report; a capability that names a dialect
|
||||
- [ADR 0166](0166-the-container-runtime-is-a-node-seat-and-the-host-creates-containers-through-its-holder.md) — the seat and its holder
|
||||
- [Issue 007](../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md), [research 011](../01-RESEARCH/011-the-module-graph/cases.md) cases 12–13
|
||||
- [Design 05 — the node host](../03-DESIGN/01-to-be/05-the-node-host.md)
|
||||
- mesh-host `internal/profile/detectors.go` (the runtime detector), `internal/profile/seat.go` (the hardware/state split), `internal/bootstrap/preflight.go` (the preflight that borrows it)
|
||||
-161
@@ -1,161 +0,0 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: proposed
|
||||
date: 2026-10-01
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0161-what-deserves-a-seat.md
|
||||
---
|
||||
|
||||
# 166. The container runtime is a node seat, and the host creates containers through its holder
|
||||
|
||||
## Context
|
||||
|
||||
Every container the mesh runs on a machine is created by the host, which looks for a runtime
|
||||
(`docker info`, then `podman info`) and drives that runtime's command line itself: run, inspect,
|
||||
remove, exec. Research 012 called this "detected rather than declared": the host takes over whatever
|
||||
runtime it finds. Nothing in the mesh owns the runtime. Its package came from the foundation bundle
|
||||
or was already on the machine. Its configuration file was written by hand, differs on each of the
|
||||
four machines, and is also written into by two modules that are not the runtime's
|
||||
([issue 190](../04-ISSUES/190-the-runtimes-configuration-is-written-by-modules-that-are-not-the-runtime/00-report.md)).
|
||||
Its service is declared by those same two.
|
||||
|
||||
The operator set the direction:
|
||||
|
||||
- a module for the runtime, on every machine, owning "what is needed to run containers here": its
|
||||
packages, its configuration and its service;
|
||||
- that module holds a node seat for the runtime, so a second runtime (podman) can later compete
|
||||
for the seat;
|
||||
- the host stops speaking to the runtime directly and uses the seat's holder. The host stays the one
|
||||
that decides, and the holder becomes the one that executes;
|
||||
- every container on the machine is in scope, not only the mesh's. A development environment started
|
||||
by hand, or a test database a tool runs, is legitimate. The host already calls these *strays*: 3,
|
||||
8 and 25 on three of the machines on the day of deciding;
|
||||
- the runtime's events and verbs are subjects on the bus, and the mesh's own interface is built on
|
||||
them. The third-party interface run until now was removed by hand.
|
||||
|
||||
[ADR 0161](0161-what-deserves-a-seat.md)'s test for a seat is whether the mesh's own code finds it by
|
||||
name. Here it does: the host would look up the holder on its own machine. A singular role of a module
|
||||
held once per machine is a `node-*` seat ([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)),
|
||||
in the controller's seed.
|
||||
|
||||
The constraint that decides most of this record is a cycle. The bus runs in containers. On the
|
||||
broker's machine, the broker's own container is created by the host. A holder's code served from a
|
||||
container cannot create the container that runs it. On a first machine, before the controller exists,
|
||||
nothing holds anything.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **The host calls the holder's verbs over the bus.** Rejected: with the broker down, no machine can
|
||||
create any container, including the broker's. The mesh would be unable to restart its own
|
||||
transport.
|
||||
2. **The holder picks a dialect that the host speaks itself, as with the uplink.** Rejected: the
|
||||
host would still drive the runtime, and the module would drive it too for every other caller.
|
||||
That is two programs speaking to one daemon, and they come to disagree about the same machine
|
||||
(the installer's preflight already exists to avoid this).
|
||||
3. **The holder's code runs as a supervised process on the machine and serves the seat's verbs
|
||||
twice: locally to the host, on the bus to everyone else.** Adopted.
|
||||
|
||||
## Decision
|
||||
|
||||
**`node-container-runtime` is a seat of the mesh's own, node-scoped,** in the controller's seed under
|
||||
this record. It delivers no provision; what it carries is its role's protocol: verbs its holder must
|
||||
serve ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)) and events its holder emits
|
||||
([ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md)). The runtime's module, `docker`, claims
|
||||
it and is assigned to every machine. A podman module may claim it later; one machine runs one.
|
||||
|
||||
**The seat's verbs cover every container on the machine:** list, inspect, logs, stats, start, stop,
|
||||
restart, create and remove. A mesh-held container is marked by the host's label and says which
|
||||
assignment holds it. **A container the runtime runs can be root on the machine** — privileged, a host
|
||||
path mounted, the host's network or process namespace, the runtime's own socket — so a caller other
|
||||
than the host may not create one that is any of these; only a declaration the mesh composed may ask
|
||||
for them. And the verbs that change anything are granted by name, never by a wildcard: a grant of
|
||||
every tool (the console's today) reaches the reading verbs only. Issue 193 is what a verb that trusts
|
||||
its caller costs. **Creating or removing a mesh-held container is the host's alone.** Any other
|
||||
caller is refused naming the assignment, because the host would undo it at its next apply. Starting,
|
||||
stopping or restarting one is allowed, and the answer says the host will restore what its
|
||||
declaration says. A container the mesh does not hold is the caller's to do anything with.
|
||||
|
||||
**The seat's events are the runtime's own** — a container created, started, stopped, died, removed,
|
||||
its health changed. They are emitted on the seat's subjects, so every holder emits the same events and
|
||||
no reader depends on which runtime holds the seat. As
|
||||
[ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||
decides, the subjects are issued by the controller, not composed by the module.
|
||||
|
||||
**The holder's code is a supervised process, not a container** ([ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)).
|
||||
A runtime cannot be run by the thing it runs. The process serves the seat's verbs on the bus to the
|
||||
console, to tools and to the mesh's interface. The same verbs are served on a local socket on the
|
||||
machine, which only the host may use. **The host creates, inspects and removes its containers
|
||||
through that socket and nothing else.** If the holder does not answer, the host creates nothing. It
|
||||
says so in its report, naming the seat. It never falls back to the command line.
|
||||
|
||||
**A container needs the seat held on its machine.** An assignment that delivers a container on a
|
||||
machine whose runtime seat is unheld is refused, naming the seat and its candidates
|
||||
([ADR 0165](0165-container-runtime-is-what-a-machine-can-run-and-a-running-runtime-is-its-holders-health.md)).
|
||||
Mounting the runtime's socket into a container is granted by the seat, not by the capability. The
|
||||
socket's path is the holder's to state, because podman's is not docker's.
|
||||
|
||||
**The runtime module owns the runtime's configuration.** Its settings are declared with defaults
|
||||
([ADR 0164](0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md)):
|
||||
the resolver containers use, the registries it trusts, log rotation, and keeping containers through a
|
||||
daemon restart. The module is given the resolver's address and the mesh's registry as values; no other
|
||||
module writes the runtime's file or declares its service.
|
||||
|
||||
**The first machine is bootstrapped with the holder, and adopted afterwards.** The foundation bundle
|
||||
already installs the runtime's package and service. It also carries the holder's process, delivered as
|
||||
a binary the way the host is ([ADR 0142](0142-the-mesh-delivers-its-own-components-as-binaries.md)).
|
||||
When the runtime module is assigned, it adopts what the bundle made, as the store and broker modules
|
||||
adopt theirs ([ADR 0078](0078-the-store-and-broker-are-modules.md)).
|
||||
|
||||
## Consequences
|
||||
|
||||
- **The migration on the running mesh has a fixed order:**
|
||||
1. Each machine's hand-written configuration is read, because the module's defaults replace what
|
||||
differs.
|
||||
2. In one push per machine: the resolver module and the private network stop writing the
|
||||
runtime's file ([issue 190](../04-ISSUES/190-the-runtimes-configuration-is-written-by-modules-that-are-not-the-runtime/00-report.md)),
|
||||
and the runtime module is assigned and adopts the runtime, its file and its service. Split in
|
||||
two, either the controller refuses two modules declaring one path, or a machine is left with
|
||||
nothing setting `dns` and `live-restore`.
|
||||
3. The controller seeds the seat and enforces the container requirement.
|
||||
4. The host releases the version that uses the holder.
|
||||
5. The host's command-line path is removed in the release after every machine's holder answers.
|
||||
Until then, the host reports per machine which path it used.
|
||||
- **Every container the host makes depends on the holder's process.** A crash-looping holder stops
|
||||
new containers on its machine. Running containers are unaffected. The host's report names the cause.
|
||||
- The process form of a module's own code must serve tools on the live mesh before this ships. Only the
|
||||
showcase declares it, and [issue 117](../04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/01-diagnosis.md)
|
||||
found the showcase's tools declared in a form nothing runs. The runtime module is the first whose
|
||||
tools cannot fall back to a container.
|
||||
- A user interface subscribing to events directly does not exist. Today a reader of events is a module
|
||||
that consumes them. The mesh's container view is a module, or waits for that path.
|
||||
- [ADR 0005](0005-the-node-host.md) ("a container runtime is detected, not chosen") and
|
||||
[ADR 0006](0006-the-substrate-and-the-control-plane.md)'s matching line describe the mechanism this replaces: on
|
||||
acceptance, each gets a dated note saying the runtime is now a seat's holder, as the decision
|
||||
records' rule for a moved mechanism requires. Design 05 and design 26 are amended after acceptance.
|
||||
- The operator's decision to remove the third-party interface by hand needs no mechanism. No
|
||||
module-retires-module rule is introduced.
|
||||
- **What got harder:** the host gains a dependency it did not have, and a first machine's bundle gains
|
||||
a component. The direct path was simpler and is what makes a runtime a black box to the rest of the
|
||||
mesh.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| The seat is the mesh's own, node-scoped, with its verbs and events | A catalogue test on the default seats; registration refuses a claimant that does not serve every verb (design 33's existing check) |
|
||||
| Creating or removing a mesh-held container is the host's alone | A test of the runtime module's verbs: create or remove of a container carrying the host's label, from any caller but the host's socket, is refused naming the assignment; the same verbs on an unlabelled container succeed |
|
||||
| No caller but the host creates a container that is root on the machine | A test of `create` from the bus: privileged, a host path, the host's namespaces and the runtime's socket are each refused; the same request on the host's socket is accepted. A broker test: a grant of every tool does not reach a changing verb |
|
||||
| The host uses the holder and never the command line | A host test with a fake holder on the local socket: every container operation goes to it, and with the holder absent the apply creates nothing and reports the seat; after step 5, the host carries no command-line runtime code (checked by build: the package is gone) |
|
||||
| A container needs the seat held | A resolution test refusing a containerised assignment on a machine with the seat unheld, naming the seat |
|
||||
| Socket mounts are granted by the seat | A catalogue test: a module mounting the runtime's socket on a machine whose holder states a different path is refused |
|
||||
| No other module writes the runtime's file | The existing collision check, once the private network's computed resources are inside it ([issue 190](../04-ISSUES/190-the-runtimes-configuration-is-written-by-modules-that-are-not-the-runtime/00-report.md)) |
|
||||
| Live | `seats` lists `node-container-runtime` held on every machine; the node listing shows each machine's containers, strays included, from the seat's `list` verb; a container started by hand appears as an event on the bus |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md), [ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md), [ADR 0161](0161-what-deserves-a-seat.md)
|
||||
- [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md), [ADR 0142](0142-the-mesh-delivers-its-own-components-as-binaries.md), [ADR 0078](0078-the-store-and-broker-are-modules.md), [ADR 0005](0005-the-node-host.md)
|
||||
- [ADR 0164](0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md), [ADR 0165](0165-container-runtime-is-what-a-machine-can-run-and-a-running-runtime-is-its-holders-health.md), [issue 190](../04-ISSUES/190-the-runtimes-configuration-is-written-by-modules-that-are-not-the-runtime/00-report.md)
|
||||
- [Design 26 — the seats](../03-DESIGN/01-to-be/26-the-seats.md), [design 33 — the tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)
|
||||
- mesh-host `internal/apply/apply.go` (the runtime lookup and the command line it drives)
|
||||
-2
@@ -9,8 +9,6 @@ extends: 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under
|
||||
|
||||
# 175. One tool runtime per node serves every module's tools, on the host side
|
||||
|
||||
> **The mechanism changed — 2026-10-02, by [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md).** Everything decided here stands: one runtime per node, host-side, every module's tools and every held seat's verbs on the memberships' subjects, root the module's concern, any node calls any tool, the console its serving mode. What moved is how the runtime brings a bundle to life. Decision 3 and the consequence *the node tools runtime needs an interpreter on the machine* read as though a bundle were always interpreted code the runtime imports; a tools bundle is now a process in any language that speaks MCP over stdio to the runtime, and importing a TypeScript bundle is the shortcut, not the contract.
|
||||
|
||||
## Context
|
||||
|
||||
A module's tools are code the module wrote, one function behind each verb, served on the subjects
|
||||
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-10-02
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
|
||||
---
|
||||
|
||||
# 188. A module's own code is bundles in any language, and a tools bundle speaks MCP to the runtime
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) put one
|
||||
tool runtime on every node and said a module brings its tools as a bundle. The runtime that exists
|
||||
is written in TypeScript and brings a bundle to life by **importing it into its own process**, which
|
||||
only JavaScript can be. The SDK ([ADR 0039](0039-what-the-sdk-holds-and-refuses.md)) is one
|
||||
TypeScript package. The builder knows three toolchains — TypeScript, Go, Python — and every one of
|
||||
the 35 catalogue modules with tools wraps them in a container on the runtime's TypeScript image.
|
||||
Nothing in the records says a module's code may be written in anything else, and nothing refuses a
|
||||
module that wraps its own code in an image to get around that.
|
||||
|
||||
The operator's direction, stated on 2026-10-02 and repeated: *the SDK is the most important part;
|
||||
we must not limit developers; tools can be written in any possible language — Rust, C, Go,
|
||||
JavaScript. A service in Go or Rust as a systemd unit must be possible too. One module can deliver
|
||||
all kinds of bundles: one for its tools, one for a seat's implementation, one for a daemon. Support
|
||||
the bare minimum first, as a skeleton; a full implementation comes when the work requires it.*
|
||||
|
||||
Measured against that: the `bundle` artifact kind already names a language and the `process`
|
||||
resource already runs a command from an unpacked bundle as a unit the host writes
|
||||
([ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)), so a Go
|
||||
daemon as a native service is possible today and one module in the catalogue does it. What is not
|
||||
possible is a tool in any language but one, and what is not written is that any of this is the
|
||||
rule.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **One SDK, one language, as now.** Rejected: it limits who can write a module to one
|
||||
ecosystem, which the operator declines, and it is what made every module's tools a container
|
||||
on one image.
|
||||
2. **A full bus client per language.** Each SDK speaks the bus itself; the runtime only
|
||||
supervises. Rejected: a transport in every SDK is what ADR 0039 refuses, and a bus change
|
||||
would then rebuild every module in every language — the cascade, multiplied.
|
||||
3. **A tools bundle is a process the runtime launches and speaks a small local protocol to,
|
||||
and that protocol is MCP over stdio.** Chosen. The runtime already speaks MCP outward (the
|
||||
console); speaking it inward to a child process is the same vocabulary. Every language that
|
||||
has an MCP server library can write a tools bundle today with no mesh SDK at all, and the
|
||||
mesh's own SDK for a language is a thin convenience over it. The transport stays in the
|
||||
runtime, so a bus change rebuilds nothing.
|
||||
4. **A protocol of the mesh's own design.** Rejected: a second way to describe a tool, its
|
||||
schema and its call, inventing what MCP already settled, for no gain.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. A module's own code is bundles, in any language the mesh has a toolchain for, and never an
|
||||
image.** A `bundle` names its language and what it is for. Images are for third-party software a
|
||||
module installs — a database, a forge — never for code the module wrote. One module may declare
|
||||
several bundles: its tools, its implementation of a seat's verbs, a daemon, a step. Each is built
|
||||
alone and delivered alone, as [ADR 0156](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)
|
||||
already has it.
|
||||
|
||||
**2. A bundle the runtime serves is a process that speaks MCP over stdio.** The node's runtime
|
||||
launches it as the bundle names it — an interpreter and a file, or a binary — with the runtime's
|
||||
environment, asks `tools/list`, and answers each call on the bus by `tools/call`. A tool whose name
|
||||
is `<seat>.<verb>` is the module's implementation of that seat's verb; any other name is the
|
||||
module's own tool. Everything the runtime does with what it is told — subjects from the membership,
|
||||
a held seat's verbs, the `tools` answer, a bundle that fails named and the others serving — stays as
|
||||
[ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) and
|
||||
[ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||
have it. A TypeScript bundle may still be imported into the runtime's own process; that is a
|
||||
shortcut over the same contract, not a second contract, and a TypeScript bundle written against
|
||||
the protocol is served the same way as any other.
|
||||
|
||||
**3. A bundle that is a service is a `process`**, run by the host as a unit, in whatever language it
|
||||
is compiled from, exactly as the host's own bundle already is. Nothing new is decided here; it is
|
||||
said so that it is the rule and not an example.
|
||||
|
||||
**4. One thin SDK per language, and the test of ADR 0039 applies to each.** An SDK for a language
|
||||
holds the MCP-over-stdio loop, the tool-definition type and the few primitives a module's code
|
||||
needs; it holds no transport, no module's client and nothing volatile. Where a language has a
|
||||
sound MCP library, the SDK wraps it rather than re-implementing it. The languages are those that
|
||||
make sense to write a module in; the first set is TypeScript, Go, Python, Rust and C, and the set
|
||||
grows when a module needs one, not before.
|
||||
|
||||
**5. Skeleton first.** Each piece — a toolchain, a launcher, an SDK — exists at the bare minimum
|
||||
that lets one bundle in that language be built, delivered and answer one tool on the live mesh.
|
||||
Anything beyond that is added when a module needs it. A skeleton that is not proven by one bundle
|
||||
answering is not a skeleton; it is a promise.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The runtime gains a launcher beside its loader. The loader, the memberships, the seats and the
|
||||
failure handling built for ADR 0175 stand; the launcher is the one new step.
|
||||
- The builder gains a toolchain per language, each at the skeleton: compile, pack, name the
|
||||
entrypoint. Rust and C are new; a language that compiles to a binary says its operating system
|
||||
as a Go bundle already does.
|
||||
- An existing MCP server in any language is already a valid tools bundle. What the mesh adds is
|
||||
the subjects, the seats and the memberships around it.
|
||||
- The gate [to-be 38](../03-DESIGN/01-to-be/38-building-the-operators-machine.md) WP2 adds —
|
||||
refusing a tools container built on the runtime's image — widens: a module whose own code is
|
||||
an image artifact is refused at registration, naming this record.
|
||||
- What got harder: a tools bundle is now a process per module on the node rather than code in
|
||||
one process, so the runtime supervises children and restarts one that dies. The one-process
|
||||
shape ADR 0175 counted on for the TypeScript shortcut remains available for it.
|
||||
- ADR 0039's "what the SDK holds" now reads per language; its refusals are unchanged and are the
|
||||
reason option 2 was rejected.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A module's own code is never an image | the catalogue's registration check: a manifest with a `bundle` kind of own code *and* an image artifact built from the module's own directory is refused, naming this record |
|
||||
| A tools bundle in a language other than TypeScript answers on the bus | the runtime's tests: a bundle written against the protocol in a second language, launched, its tool called over a real bus |
|
||||
| A TypeScript bundle written against the protocol is served like any other | the same tests, with the TypeScript shortcut off |
|
||||
| Each SDK is thin | each SDK's own README states what it holds under ADR 0039's test, and its size is in the mesh's records |
|
||||
| Live | a tool in a compiled language answers from the node's runtime on one machine |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md),
|
||||
[ADR 0039](0039-what-the-sdk-holds-and-refuses.md),
|
||||
[ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md),
|
||||
[ADR 0156](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md),
|
||||
[ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||
- [To-be 38](../03-DESIGN/01-to-be/38-building-the-operators-machine.md) — the work packages this
|
||||
record widens
|
||||
- The Model Context Protocol's stdio transport — the local protocol a tools bundle speaks
|
||||
@@ -0,0 +1,130 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-02
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
|
||||
---
|
||||
|
||||
# 188. A provider declares what it derives for each consumer, and the mesh tells both ends
|
||||
|
||||
## Context
|
||||
|
||||
An arrangement between a consumer and a provider is delivered entirely by the mesh. Where the
|
||||
provider is, which port it answers on, what name the consumer must present, where its password
|
||||
is — each arrives as a fact the consumer reads from its binding, or as `${bound:…}` filled into a
|
||||
file before the declaration leaves the control plane. The provider invents none of it and hands
|
||||
none of it back ([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)).
|
||||
|
||||
One kind of value escapes that. Where the **provider names the resource** — a bucket, a database,
|
||||
a vhost — the name is derived from the consumer, per consumer, and the mesh has no way to carry
|
||||
it. `serves` is a literal block in the provider's definition: the same values for every consumer.
|
||||
A provisioner's contract takes a provision and returns nothing. So a value the mesh's own rule
|
||||
produced reaches neither end as a statement; it is recomputed at one end and transcribed at the
|
||||
other.
|
||||
|
||||
The object store is the instance ([issue 124](../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md)).
|
||||
Its provisioner normalises the login the mesh minted into a bucket name and creates, checks and
|
||||
removes exactly that; the rule lives in twenty lines of the module's own TypeScript. Its three
|
||||
consumers each write the answer into their own definition by hand. Two transcribed it correctly;
|
||||
one named a predecessor's bucket, and would have authenticated successfully and been refused on
|
||||
every object, which reads like a credential fault and is not one.
|
||||
|
||||
Even corrected, the transcriptions are wrong in a second way. Each is `mesh-<node>-<slug>`, so
|
||||
each **names the machine the module happens to run on today** — a definition stating a fact about
|
||||
one installation, which [ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
|
||||
forbids and whose check does not catch because the name is not a domain. Move any of the three to
|
||||
another machine and its configuration points at a bucket its key cannot open.
|
||||
|
||||
The shape is not the object store's. A database provisioner that prefixed names, a queue provider
|
||||
that scoped vhosts, any provider that derives a resource from who is asking: each forces the
|
||||
consumer to reproduce somebody else's rule and keep it in agreement by hand.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. A served value may name the consumer the mesh is serving.** A `serves` block, which is
|
||||
literal today, may interpolate the mesh's own statement of who the consumer is:
|
||||
|
||||
- `${consumer:as}` — the identity the mesh minted for this consumer, exactly as the login it is
|
||||
told to present ([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md));
|
||||
- `${consumer:as:dns}` — the same identity written as a DNS label.
|
||||
|
||||
Nothing else. **The mesh learns no protocol here; it spells its own name in an alphabet it already
|
||||
knows.** The identity is the mesh's, minted by the mesh, already capped at twenty characters
|
||||
because of what an S3 access key accepts; `dns` is that same name with its separator written `-`
|
||||
instead of `_`, which is the whole of the difference between the mesh's identifier alphabet and
|
||||
the one buckets, vhosts and hostnames use. A provider that needs a prefix or a suffix writes it
|
||||
around the placeholder, because a served value is a string.
|
||||
|
||||
The rejected alternative is **the provider returning values from provisioning** — the natural
|
||||
channel, since the provider is what derived them. It is rejected for three reasons, in order of
|
||||
weight. It inverts the delivery the mesh is built on: a grant would carry data the provider wrote
|
||||
rather than only data the mesh minted, and [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)
|
||||
removed exactly that second path once already. It makes a consumer's declaration incomplete until
|
||||
its provider's reconcile loop has run, so a consumer could not be composed before a provider
|
||||
answered — a bootstrap order the mesh does not have and does not want. And it puts the rule where
|
||||
nothing can check it: a value that arrives from a running process cannot be refused at resolution,
|
||||
only discovered wrong later, which is the failure this record exists to end.
|
||||
|
||||
**2. The mesh resolves it once, per consumer, and tells both ends from the one resolution.** At the
|
||||
moment a consumer's declaration is composed, the mesh knows exactly who the consumer is. There, and
|
||||
only there, the placeholders are filled. The result reaches:
|
||||
|
||||
- the **consumer**, as the served facts in its binding file and as `${bound:<provision>:<key>}` in
|
||||
any file it writes — unchanged mechanisms, carrying one more key;
|
||||
- the **provider**, as `serves` on that consumer's entry in its contributions file, so the
|
||||
provisioner is *told* the name rather than recomputing it.
|
||||
|
||||
**The provider stops deriving in code and starts declaring.** One statement, filled once, delivered
|
||||
to both ends: the two cannot disagree, because there is no second computation to disagree with.
|
||||
|
||||
**3. A served value stays settled before it is per-consumer.** Settings still compose into `serves`
|
||||
([ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)), and the consumer
|
||||
placeholders are filled after that, so an operator may set a prefix and the mesh still derives the
|
||||
rest. A `${consumer:…}` naming a fact or an alphabet the mesh does not have is refused when the
|
||||
definition is parsed, with what it may say.
|
||||
|
||||
**4. A consumer may no longer name the resource its provider derives.** With the value delivered,
|
||||
a literal in a consumer's definition is not merely redundant — it is the one thing that can
|
||||
disagree with what the provider will actually create. The three object-store consumers lose their
|
||||
hand-written bucket names in this change.
|
||||
|
||||
## Consequences
|
||||
|
||||
- One more thing a definition may say, and one less thing a module may be wrong about. The
|
||||
vocabulary grows by a placeholder; the catalogue loses three literals that named this
|
||||
installation's control node.
|
||||
- A provider's naming rule becomes readable in its definition instead of in its source. `minio`'s
|
||||
`bucketFor` goes; the manifest says `"bucket": "${consumer:as:dns}"` and the provisioner uses
|
||||
what it is given.
|
||||
- A provider that already serves consumers keeps serving them: the derived value equals what the
|
||||
code derived, so no bucket, database or login changes name. This is a change of **who says it**,
|
||||
not of **what is said**.
|
||||
- The mesh now holds a rule in another system's alphabet — one rule, `dns`, stated once. A second
|
||||
alphabet is a decision, not an addition: the cost of each is that the mesh must be right about
|
||||
somebody else's naming, and that cost is only worth paying where the mesh already mints the name.
|
||||
|
||||
## How this is checked
|
||||
|
||||
- A served value naming an unknown fact or alphabet is refused at parse, with the list of what it
|
||||
may say — tested on both halves of the message.
|
||||
- Resolving a consumer whose provider derives a value puts that value in the consumer's binding
|
||||
file, in its `${bound:…}` substitutions, and in the provider's contributions entry for that
|
||||
consumer — one test asserting the three agree, because agreeing is the whole point.
|
||||
- Two consumers of one provider on one machine get two different derived values, and neither gets
|
||||
the other's.
|
||||
- A catalogue-wide test refuses a consumer definition that writes a literal where its provider
|
||||
derives: the provider's `serves` names the key, so the catalogue can say which definitions
|
||||
transcribe one.
|
||||
- `dns` is checked against the identity the mesh actually mints, not against an invented string:
|
||||
the test derives an identity with `ConsumerIdentity` and asserts the label it becomes.
|
||||
|
||||
## References
|
||||
|
||||
- [issue 124 — a consumer cannot be told a value its provider derived for it](../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md)
|
||||
- [ADR 0048 — a provider creates the credential the mesh minted, and seals nothing](0048-a-provider-creates-the-credential-the-mesh-minted.md)
|
||||
- [ADR 0049 — a consumer's identity fits the tightest backend](0049-a-consumers-identity-fits-the-tightest-backend.md)
|
||||
- [ADR 0174 — a node varies a module through settings and kept regions, never through an edit](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)
|
||||
- [ADR 0155 — a definition names no installation, and how that is checked](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
|
||||
- [design 27 — a module requires, the mesh resolves](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md)
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-02
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md
|
||||
---
|
||||
|
||||
# 189. The store keeps what the records name, and a maintenance step holds its writers still
|
||||
|
||||
## Context
|
||||
|
||||
The mesh's artifact store has never collected anything
|
||||
([issue 108](../04-ISSUES/108-the-registry-has-no-garbage-collection-once-it-has-two-doors/00-report.md)).
|
||||
Every build pushes another layer set; nothing has ever removed one. The predecessor ran a routine
|
||||
on a timer — stop the registry, collect, start it — and the conversion carried the settings that
|
||||
routine depends on without the routine, because the routine was a script beside the module and not
|
||||
a resource in it. The store now holds fifty-three repositories on the machine that serves
|
||||
everything else, and the only outcome of leaving it is a full disk reported as somebody else's
|
||||
failure.
|
||||
|
||||
Three things stood in the way, and the issue names all three.
|
||||
|
||||
**Nothing in the mesh's vocabulary expresses a maintenance window.** The collector requires every
|
||||
writer stopped while it runs. A `run-once` step runs *beside* containers, not instead of them, and
|
||||
a scheduled step is the same container on a cadence. There is no way for a module to say *hold this
|
||||
container of mine still while this runs*.
|
||||
|
||||
**Deletion is not enabled, and the door it would be enabled on has no accounts.** The store is
|
||||
internal, reached by name over the overlay, trusted because being on that network is the permission
|
||||
([ADR 0082](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md)). The predecessor
|
||||
kept deletion behind an authenticated door, which it could, having one.
|
||||
|
||||
**Nothing says what may be removed.** The registry's own answer — collect everything no tag names —
|
||||
is wrong here. The mesh pushes each artifact under one moving tag and pins machines by digest, so
|
||||
every build but the newest is untagged and some machine may still be running it.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. Deletion is enabled on the store's one door, and the overlay stays the permission.** The
|
||||
objection dissolves on inspection: that door **already accepts a push**, and a writer who can push
|
||||
can replace any tag in the store with anything it likes. Delete takes nothing a push did not
|
||||
already have, and the machines that can reach the door are the ones the mesh's own filter admits
|
||||
([ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)). Putting an authenticated
|
||||
door in front of deletion while leaving push open would be a lock on the window beside an open
|
||||
door, and it would cost the thing ADR 0082 bought: a store every machine can reach without a
|
||||
credential to distribute first.
|
||||
|
||||
**2. The mesh deletes what it made and no longer keeps; the store reclaims the bytes.** Two halves,
|
||||
each doing what only it can.
|
||||
|
||||
The **mesh** decides. It does not need to enumerate the store to do it — it has never put anything
|
||||
there it did not record, so **every digest it could remove is already in its own build records**.
|
||||
It deletes those manifests through the store's door, by digest, and remembers that it did.
|
||||
|
||||
The **store** reclaims. A deleted manifest frees no bytes until the registry's own collector walks
|
||||
the storage with nothing writing to it, so the module declares that collector as a scheduled step
|
||||
with the server held still for its duration. Plain collection, not `--delete-untagged`: what the
|
||||
mesh keeps is still a manifest in the store, so it is still referenced, so its blobs stay — the
|
||||
dangerous flag is not needed at all once the mesh is the one deciding.
|
||||
|
||||
**3. What the mesh keeps, stated as three reasons rather than a number.** A digest is kept because:
|
||||
|
||||
- **a definition names it** — every artifact reference in any module's current recorded manifest,
|
||||
which is what the mesh would hand a machine now. No age limit: this is the floor;
|
||||
- **the mesh can still go back to it** — every artifact of the **five most recent successful
|
||||
builds** of each module, so a release that turns out wrong has somewhere to return to;
|
||||
- **nothing else.** An artifact older than that, which no definition names, is what the store is
|
||||
carrying for no stated reason.
|
||||
|
||||
A digest the mesh did not record making is never touched. That is not a safety margin, it is the
|
||||
whole rule restated: the mesh removes what it put there and can account for, and the images genesis
|
||||
pushed before any record existed are exactly what this must not reach
|
||||
([04-ISSUES/102](../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md), F4).
|
||||
|
||||
**4. A scheduled step may hold its module's own containers still while it runs** —
|
||||
`while-stopped`, naming resource ids in the same module. The host stops each, runs the step, and
|
||||
starts them again **whatever the step did**, including when it failed or the host was interrupted.
|
||||
Three boundaries:
|
||||
|
||||
- **Its own module's containers only.** A module that could quiesce a neighbour could stop the
|
||||
mesh; a maintenance window is a statement about one service's own insides.
|
||||
- **Scheduled steps only, not `run-once`.** At apply time the host already has a window: the
|
||||
declaration is applied in order and a step gates what follows, so a one-time offline migration
|
||||
says *before* rather than *instead of*. A recurring window is the case order cannot express.
|
||||
- **Restoring is not conditional.** A step that fails must leave the service running; the whole
|
||||
risk of this field is a window that never closes.
|
||||
|
||||
**5. The sweep runs where the records change — after a build the mesh recorded.** That is the
|
||||
moment new bytes landed and the moment the keep set moved, and it needs no new timer. The
|
||||
store's collection runs nightly, because reclaiming is slow and the thing it reclaims is already
|
||||
unreferenced.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Disk stops growing without bound on the machine that serves the mesh. That is the whole point
|
||||
and it has no other way to be true.
|
||||
- A machine behind by more than five builds of a module, which recreates a container, cannot pull
|
||||
what it was running. It is already a machine the mesh reports as behind, and the answer is the
|
||||
one the mesh already gives it: the current declaration. Stated here rather than discovered.
|
||||
- The store is a little less of a museum. A digest in an old build record may no longer be
|
||||
fetchable, and the record still says what that build made — the record is history, not an
|
||||
index of what is on disk. The collected mark is kept beside it so the two can be told apart.
|
||||
- `while-stopped` is a second thing the host does to a container it did not start this pass. It is
|
||||
deliberately the narrowest form: the module's own, by id, restored unconditionally.
|
||||
- The store is briefly unavailable each night, for as long as collection takes. Everything that
|
||||
pulls from it retries; nothing in the mesh treats a momentary store as a failure
|
||||
([ADR 0185](0185-a-control-plane-behind-its-seats-row-serves-what-it-can.md)).
|
||||
|
||||
## How this is checked
|
||||
|
||||
- The host: a scheduled step with `while-stopped` stops the named containers before the run and
|
||||
starts them after; it starts them again **when the step fails**; it refuses an id that is not a
|
||||
container of the same module, its own id, and `while-stopped` on a `run-once` step. Each refusal
|
||||
is tested for what it says, not only that it says something.
|
||||
- The controller: given build records and current manifests, the keep set holds every reference a
|
||||
manifest names and every reference of the five most recent builds per module, and nothing else;
|
||||
a reference the mesh never recorded is never in the delete set; a delete that answers 404 is
|
||||
recorded as collected rather than retried forever.
|
||||
- The sweep is tested against a fake store that records what it was asked to delete, so what is
|
||||
asserted is the decision and not the registry's behaviour.
|
||||
- Live: the store's size before and after the first nightly collection, read from the machine.
|
||||
|
||||
## References
|
||||
|
||||
- [issue 108 — the registry has no garbage collection](../04-ISSUES/108-the-registry-has-no-garbage-collection-once-it-has-two-doors/00-report.md)
|
||||
- [ADR 0082 — the registry is reached by name and trusted by the overlay](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md)
|
||||
- [ADR 0053 — a step that runs on a schedule](0053-a-step-that-runs-on-a-schedule.md)
|
||||
- [ADR 0156 — an artifact is what a build produces, and the store is named for its scope](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)
|
||||
- [design 32 — what a module declares](../03-DESIGN/01-to-be/32-what-a-module-declares.md)
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-02
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
|
||||
---
|
||||
|
||||
# 190. A seat's work is shared by its holders, and building is the first such role
|
||||
|
||||
## Context
|
||||
|
||||
Work addressed to a role goes to the seat's `accept` subjects, on a per-seat work queue
|
||||
([design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §1, [ADR 0041](0041-events-are-a-relationship.md)).
|
||||
The holder's worker on that queue is already a queue group — *"even though the seat guarantees one
|
||||
holder … the day somebody allows two holders for throughput, every message is processed twice with
|
||||
nothing reporting it"* — and design 25 already says what a build queue shared by several machines is:
|
||||
*a seat's `accept` subjects, on a work queue with a queue group of holders*. The mechanism was drawn.
|
||||
Two things stopped it being used.
|
||||
|
||||
First, the build role is a **mesh-scoped** seat, `mesh-build-machine`, so there is one holder in the
|
||||
whole mesh ([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md):
|
||||
*the mesh's single build machine*). Second, the worker is a push consumer with **one delivery in
|
||||
flight** — set so after 2026-10-01, when a push consumer handing out many at once left twenty-six of
|
||||
forty-three asks undelivered ([issue 175](../04-ISSUES/175-an-announcement-behind-a-long-build-comes-back/00-report.md)) —
|
||||
and one in flight on a shared consumer is one build at a time across every holder there could be.
|
||||
|
||||
Measured on 2026-10-02: a change to code comments in the tool runtime rebuilt its thirty-five
|
||||
dependent images, one after another, on one machine, for about half an hour, while three other
|
||||
machines with a container runtime sat idle; the work the mesh wanted next waited behind it. The
|
||||
operator's words: *this is our first occurrence of a mesh advantage* — and: *make sure the setup is
|
||||
done generically, so if another module also requires mesh functionality it can re-use the pattern.*
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **A second build machine by configuration** — a concurrency setting on the one holder, or a second
|
||||
holder admitted by hand. Rejected: a setting on one machine shares nothing, and a second holder
|
||||
of a mesh-scoped seat contradicts what a mesh seat means.
|
||||
2. **A build-specific dispatcher** — the controller choosing a machine per build and asking it by
|
||||
name. Rejected: it reinvents the queue the bus already is, it makes the controller a scheduler,
|
||||
and it is specific to building; the next role needing the same would build its own.
|
||||
3. **A seat's work is shared by its holders, and the build role becomes node-scoped.** Chosen. It is
|
||||
what the bus was drawn to do, it is one rule for every role rather than one for building, and
|
||||
"the machines that are online and hold the seat" is exactly the set a queue group's members is.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. Work asked of a seat is taken by whichever of its holders is idle.** Every holder of a seat with
|
||||
`accepts` reads the seat's one work queue; a node-scoped seat held on several machines has several
|
||||
holders, and an ask goes to one of them. The asker addresses the role — `mesh.seat.<seat>.accept.<verb>`
|
||||
— and never a machine. The outcome, the role's own event, says which machine did the work (`on`), as a
|
||||
build's already does ([ADR 0157](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)).
|
||||
|
||||
**2. A holder takes one ask at a time, when it is idle, by pulling.** The worker is a pull consumer:
|
||||
a holder fetches one ask, works it, acknowledges, fetches the next. The server never hands an ask
|
||||
to a busy holder, so a slow machine never holds work an idle one could take — the fault issue 175
|
||||
found in push delivery is removed by the shape rather than by a limit, and the one-in-flight limit
|
||||
that made the shared queue serial goes with it. A holder that dies mid-work leaves its ask to be
|
||||
redelivered to another, as today.
|
||||
|
||||
**3. Work that must run on one particular machine is not a work queue.** That is a node seat's verb
|
||||
asked of that machine ([design 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §4), and
|
||||
nothing here changes it. A role's work queue is for work whose result is the same whichever holder
|
||||
does it: a build is, because what comes out is published by digest to the mesh's store.
|
||||
|
||||
**4. This is one pattern, not one role's.** Any module that declares a node-scoped seat with
|
||||
`accepts` gets decisions 1 and 2 with no further mechanism: the controller derives the queue and the
|
||||
worker, the holders pull, the module's manifest says what every holding machine must have. The
|
||||
build agent is the first; a module needing work done *somewhere on the mesh* — a scan, a
|
||||
conversion, a fetch — declares a seat of its own the same way
|
||||
([ADR 0126](0126-a-module-declares-its-own-seats.md)).
|
||||
|
||||
**5. Building is the first such role.** The build role is `node-build-agent`, scope node, with the
|
||||
same `build` ask and the same `started`, `built` and `log.<id>` events as before. Its holder is the
|
||||
`build-agent` module: the builder as it is — a container runtime, the artifact store and the package
|
||||
registry resolved as provisions, a workspace, the bus credential — assignable to every machine that
|
||||
has a container runtime. `mesh-build-machine` and the `builder` module are retired when the new
|
||||
holder is assigned where the old one was. The tiered plan ([ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md))
|
||||
is unchanged: a tier's asks go out together and are now worked together.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A tier of thirty-five images is built by as many machines as hold the seat and are online. A
|
||||
machine that is off builds nothing and blocks nothing.
|
||||
- Every holding machine fetches base images from the store and pushes what it builds; the store is
|
||||
reached as a provision, so this is what the provision was for. A machine with a slow link builds
|
||||
slowly, and takes fewer asks for it, which is the point of pulling.
|
||||
- A build's outcome carries which machine built it, so a build that fails on one machine and not
|
||||
another is a fact the record shows, not a mystery.
|
||||
- What got harder: a build's cache is per machine, so a cold machine pays the first pull of every
|
||||
base it has never seen; the artifact store is now asked by several machines at once, and the
|
||||
package registry likewise. Both are provisions and both are made for that.
|
||||
- ADR 0121's *"the mesh's single build machine"* and ADR 0162's *"built by whichever build machine
|
||||
is running — the only one there could be"* were true and are no longer; both records carry a note.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| Two holders of one seat each take one of two asks, and a third ask waits for the first to be idle | the controller's test over the work queue against a real bus: two machines bound to one worker, three asks |
|
||||
| An ask is never delivered to a busy holder | the same test: the busy holder's ask count stays at one until it acknowledges |
|
||||
| A holder that dies mid-work leaves its ask for another | the same test, one holder closed mid-ask |
|
||||
| The asker names no machine | the controller's seat table: `node-build-agent` accepts `build` and the asking side publishes to the seat's accept subject, as the existing tests of `build` already require |
|
||||
| Live | `builds` shows a tier's builds `on` more than one machine within one plan; `seats` shows `node-build-agent` held on every machine with a container runtime — *held on all four machines and a build taken by a workstation's agent, 2026-10-03* |
|
||||
|
||||
## References
|
||||
|
||||
- [Design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §1 and §5 — the work queue and the queue
|
||||
group of holders this uses as drawn
|
||||
- [Design 18](../03-DESIGN/01-to-be/18-building-a-module.md) — building a module, amended for
|
||||
where a build runs
|
||||
- [ADR 0041](0041-events-are-a-relationship.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md),
|
||||
[ADR 0126](0126-a-module-declares-its-own-seats.md), [ADR 0157](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md),
|
||||
[ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md)
|
||||
- [Issue 175](../04-ISSUES/175-an-announcement-behind-a-long-build-comes-back/00-report.md) — why the
|
||||
worker had one in flight, and why pulling removes the cause rather than the symptom
|
||||
@@ -1,120 +0,0 @@
|
||||
---
|
||||
topic: the tiers
|
||||
status: accepted
|
||||
date: 2026-10-03
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
supersedes-in-part:
|
||||
- 0066-public-routing-is-name-agnostic.md
|
||||
- 0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md
|
||||
---
|
||||
|
||||
# 191. The mesh's resolver holds only the mesh's own names; a public name resolves publicly
|
||||
|
||||
> **Progressive insight — 2026-10-03.** The first implementation told the mesh's names from public
|
||||
> ones by their spelling — a name ending in the mesh suffix — and this record said so: the Decision
|
||||
> read *"only names under its own suffix"*, and the roster check *"every name the roster carries ends
|
||||
> in the mesh suffix"*. The mesh needs no such test, nor any per-route name: domains are a node's. A
|
||||
> node has **one internal domain**, `<node>.internal`, and every route on it is a name under that domain
|
||||
> (ADR 0151), answered by one wildcard per node; a node has **one or more public domains**, which public
|
||||
> DNS answers. So the mesh's resolver holds the nodes' internal domains and nothing else, and the roster
|
||||
> carries the machines and no routed name. Both sentences now say that; what was decided — a public
|
||||
> name is never given a private answer — is unchanged.
|
||||
|
||||
## Context
|
||||
|
||||
**[ADR 0066](0066-public-routing-is-name-agnostic.md) published every routed name into internal
|
||||
resolution, mesh-wide, at the address of the node that serves it.** The reason was an internal
|
||||
certificate authority in the lab: it validates by connecting to the name it certifies, and a routed
|
||||
public name that nothing inside the mesh resolved could not be certified.
|
||||
[ADR 0151](0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md) kept it:
|
||||
*the roster publishes it as itself, once, at the serving node's address.*
|
||||
|
||||
**So every machine's resolver answered public names with private-network addresses.** On a
|
||||
production mesh on 2026-10-03, each machine's hosts region carried 47 lines of the form
|
||||
`<private address> <label>.<public domain>` — every public name of the control-node at its tunnel
|
||||
address, every public name of the home server at its own. For the machines themselves this is merely
|
||||
a detour: their traffic to a public name goes through the tunnel instead of the internet.
|
||||
|
||||
**For anything that is not a member it is an outage.** The home server's resolver also answers its
|
||||
LAN — a listen address added as a setting on 2026-10-02. A phone on that LAN asked for the mail
|
||||
server's public name, was given the control-node's tunnel address, and could not connect:
|
||||
*couldn't connect to host, port: 10.10.0.1:143*. Every public name of the mesh failed the same way for
|
||||
every non-member on that LAN — a phone, a television, a guest — while every check the mesh runs
|
||||
reported success, because every check runs from a member.
|
||||
|
||||
**And the reason for publishing them is gone.** ADR 0151 gave every route an internal name,
|
||||
`<label>.<serving node>.internal`, under the node's own name. It resolves inside the mesh without any
|
||||
entry of its own, the proxy serves it, and the internal authority certifies it — the proxy has two
|
||||
authorities since 2026-09-25: a public one for public names, the internal one for internal names.
|
||||
Measured the same day: `drive.<control-node>.internal` resolves to the control-node's tunnel address
|
||||
and answers 200 with a certificate that verifies against the internal root. Nothing the mesh runs
|
||||
needs a public name to resolve to a private address. The one consumer that did — an internal
|
||||
authority validating a public name — is the case the second authority removed.
|
||||
|
||||
The predecessor's resolver held exactly this and no more: an address per machine under `.internal`,
|
||||
and everything else forwarded to public resolvers.
|
||||
|
||||
## Considered Options
|
||||
|
||||
**1. Keep publishing public names; stop the resolver answering the LAN.** Fixes the phone and
|
||||
nothing else. The mesh would still hold a second, private answer for names the public DNS already
|
||||
answers — two answers for one name, which disagree by design and are correct in different places.
|
||||
And it forbids a reasonable setup: a home server's resolver serving its own LAN.
|
||||
|
||||
**2. Answer per source: private addresses to members, public ones to everyone else.** Split-horizon
|
||||
by client. It is what a resolver serving two audiences would need *if* the private answer were worth
|
||||
giving. It is not — option 3 shows nothing needs it — and it makes a name's address depend on who
|
||||
asks, which is the hardest kind of fault to see from a member.
|
||||
|
||||
**3. The mesh's resolver holds only the mesh's own domain.** Names under the mesh suffix — machines,
|
||||
and routes' internal names under them — resolve to private addresses. Every other name, including
|
||||
every public name the mesh serves, is forwarded and resolves publicly. Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**The mesh's resolver holds each node's internal domain and nothing else** — `<node>.internal` and
|
||||
everything under it, at that node's private address. A machine's name,
|
||||
and through it every `<label>.<node>.internal`, resolve to that machine's private address. **A public
|
||||
name is never given a private answer by the mesh**: it resolves through public DNS to the public
|
||||
address, from members and non-members alike.
|
||||
|
||||
This replaces ADR 0066's clause *"when the proxy is granted a name, the mesh publishes that name →
|
||||
the node that serves it into internal resolution, mesh-wide"*, and ADR 0151's *"the roster publishes
|
||||
it as itself, once, at the serving node's address."* Everything else in both stands: the label, the
|
||||
node's public domain, the composition, and the internal name under the serving node.
|
||||
|
||||
**Inside the mesh, a route is reached by its internal name.** A container or a validator that must
|
||||
reach a routed service inside the mesh uses `<label>.<node>.internal`; the internal authority
|
||||
certifies that name, and a public authority certifies the public one. A mesh with no public
|
||||
reachability — the lab — certifies its internal names and has no public names to resolve.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **A resolver serving a LAN is safe.** What it adds to public resolution is the mesh's own domain,
|
||||
which no public resolver answers.
|
||||
- **A member reaches a public name over the internet, as anyone does.** A route the proxy restricts
|
||||
to the private network is reached by its internal name, never by its public one — a public name
|
||||
is, by this decision, public.
|
||||
- **The internal authority certifies internal names only.** It was the only consumer of a public
|
||||
name's private answer; the proxy's second authority already took that role away from it.
|
||||
- **Public names leave every machine's hosts region** on the first push after the change.
|
||||
Containers do not move with it: the roster is not part of a container's identity
|
||||
([ADR 0148](0148-the-meshs-names-are-resolved-not-copied-into-containers.md)).
|
||||
|
||||
**How each is checked:**
|
||||
|
||||
- **The roster:** the controller's tests assert that the roster names the machines and nothing
|
||||
else — a routed name in it, public or internal, fails the build.
|
||||
- **On a machine:** asking the machine's resolver for a public name the mesh serves returns the
|
||||
public address, and asking it for that route's internal name returns the private one. Asked from a
|
||||
non-member on a LAN the resolver answers, the first must hold as well.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0066 — public routing is name-agnostic](0066-public-routing-is-name-agnostic.md), whose
|
||||
propagation clause this replaces.
|
||||
- [ADR 0151 — a route's internal name is composed under the node that serves it](0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md),
|
||||
which made the private answer unnecessary.
|
||||
- [Connectivity design §2 and §5](../03-DESIGN/01-to-be/08-connectivity.md), amended alongside this
|
||||
record.
|
||||
-122
@@ -1,122 +0,0 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-10-03
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md
|
||||
---
|
||||
|
||||
# 192. A tools bundle declares what it is given, and the runtime hands it to that bundle alone
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) put one
|
||||
runtime on every node serving every module's tools from a bundle, and
|
||||
[ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)
|
||||
said a module's own code is bundles and never an image. The two holders that moved first
|
||||
([to-be 38](../03-DESIGN/01-to-be/38-building-the-operators-machine.md) WP4) needed nothing a
|
||||
bundle does not have: a fixed path, and root. Research
|
||||
[020](../01-RESEARCH/020-what-a-bundled-tool-is-given/00-overview.md) measured the rest before
|
||||
they move: thirty-three modules still run their tools as a container on the runtime's image, and
|
||||
thirty-one of them are handed, through the container's environment and mounts, things a bundle
|
||||
has no way to receive — the module's configuration file, its own secret as a file, the service's
|
||||
address with the port the mesh chose, a provision's address, a directory of grants. Every one of
|
||||
those is a file the mesh already places on the machine or a value the controller already composes
|
||||
for the container, per module per machine, from references the manifest writes: a placed
|
||||
directory, a chosen port. And the SDK's tool contributor is a function of an environment that the
|
||||
runtime calls without one, so every bundle reads the process's four words.
|
||||
|
||||
Without a rule, each of the thirty-one would answer the question its own way, and the runtime's
|
||||
process would be the one place where every module's paths meet.
|
||||
|
||||
> **Progressive insight — 2026-10-03.** The context above calls the thirty-one remaining containers
|
||||
> tool containers handed what a bundle cannot receive. Measured the same day while building this
|
||||
> record: nine of them run only tools; three run a main of their own; twenty import, beside their
|
||||
> tools, the module's own long-running code — event handlers that subscribe on the bus and
|
||||
> provisioners that act on grants — under the module's own bus identity, and some reach their
|
||||
> service by a container network name or need a package the image installed. That code is not a
|
||||
> tool and is not this record's to move: under
|
||||
> [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)
|
||||
> §3 it is a `process` bundle, and how it is given its credential, its words and its reach is the
|
||||
> open question of [design 38](../03-DESIGN/01-to-be/38-building-the-operators-machine.md) WP4c.
|
||||
> Decision 4 applies to these containers' tools; the containers themselves go when their other
|
||||
> code has moved. The decision and its options stand.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **The bundle declares its environment on its artifact, and the mesh composes it as a
|
||||
container's.** Chosen. The tools artifact gains `env`: names to values, the values written with
|
||||
the references the composer already resolves for a container — `${dir:…}`, `${port:…}` — and
|
||||
what was a mount target becomes the host path itself. The controller composes one environment
|
||||
per bundle per machine into the runtime's declaration. The runtime hands it to that bundle's
|
||||
contributor, or to the child it launches, and to nothing else. The tool code reads the names it
|
||||
read before.
|
||||
2. **The runtime derives it from the module's placed manifest** — a conventional word per
|
||||
directory and port, no new field. Rejected: a convention the thirty-one tools must be rewritten
|
||||
to, the runtime learning the composer's job, and a module that names its file one way and a
|
||||
module that names it another needing different words regardless.
|
||||
3. **The tool asks the controller over the bus.** Rejected: a tool that cannot start until the
|
||||
bus answers fails in the one case tools exist for, and a secret crossing the bus to reach a file
|
||||
already on the machine is a disclosure for nothing.
|
||||
4. **Leave each module to its own device.** Rejected by the measurement: thirty-one modules, one
|
||||
question.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. A tools bundle says what it is given, on its artifact.** `build.artifacts[].env` names the
|
||||
words the bundle reads and their values. A value is a path or a constant, composed with the
|
||||
references a container's environment may use; **never a secret's content.** A secret reaches a
|
||||
tool the way it reaches a container: as a file the mesh places, whose path the environment names.
|
||||
A bundle that declares no `env` is given nothing beyond the runtime's own words, which is what the
|
||||
two holders that moved have.
|
||||
|
||||
**2. The mesh composes it, per bundle per machine, as it composes a container's.** The same
|
||||
references, resolved the same way, to the host's own paths. The composed environment travels in
|
||||
the node's declaration beside the bundle's archive; a change to it is a change to the bundle for
|
||||
the purpose of `restart-on`.
|
||||
|
||||
**3. The runtime hands each bundle its own environment, and nothing of another's.** A bundle
|
||||
imported into the runtime's process receives it as the argument its contributor is written to
|
||||
take; a bundle launched as a child ([ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md) §2)
|
||||
receives it as the child's environment, over the runtime's own words. The runtime's process
|
||||
environment is not where a module's words go, and a tool that reads the process's environment
|
||||
rather than the one it was handed finds the runtime's four words and no module's.
|
||||
|
||||
**4. The remaining tool containers move in one change** after this is built, each proven by its
|
||||
tools answering from the runtime, and the registration gate of
|
||||
[to-be 38](../03-DESIGN/01-to-be/38-building-the-operators-machine.md) WP2 then refuses the
|
||||
container shape for every module, as ADR 0188 already provides.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The manifest gains one field on one artifact kind; the composer gains one more thing to resolve
|
||||
with references it has; the runtime gains the hand-off and the separation. The thirty-one modules'
|
||||
tool code does not change, and their conversion is the move of a container's `env` with its
|
||||
mounts folded into host paths.
|
||||
- A tool's inputs become legible in the manifest where its container hid them in mounts: what a
|
||||
module's tools read is declared beside what the module writes.
|
||||
- What got harder: the runtime must keep thirty-one environments apart in one process, and a
|
||||
bundle's author must not reach for the process's environment. The separation is a rule the
|
||||
runtime's test holds, not a property of the language.
|
||||
- `MESH_BROKER_FILE` is not a bundle's to declare: the runtime speaks with the node's credential
|
||||
([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)), and
|
||||
a module's own bus credential went with its container.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A value in a bundle's `env` is a path or a constant, never a secret's content | the catalogue's manifest check refuses a `${secret:…}` reference in a bundle's `env`, naming this record |
|
||||
| The composer resolves a bundle's `env` as a container's | the controller's composition test: one module, one bundle with `${dir:…}` and `${port:…}` in its `env`, the declaration carrying the host paths and the chosen port |
|
||||
| Each bundle sees its own environment and no other's | the runtime's test: two bundles with different `env`, loaded in one runtime, each answering with its own words and none of the other's; the same for a launched bundle |
|
||||
| A change to a bundle's environment restarts the runtime | the composition test above, with `restart-on` naming the bundle |
|
||||
| Live | a module whose tools read a configuration file and a token file answers from the runtime on one machine with no container |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md),
|
||||
[ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)
|
||||
- Research [020](../01-RESEARCH/020-what-a-bundled-tool-is-given/00-overview.md) — the measurement
|
||||
and the options
|
||||
- [to-be 38](../03-DESIGN/01-to-be/38-building-the-operators-machine.md) — where the work is listed
|
||||
@@ -188,7 +188,8 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0185** — [A control plane behind its seat's row serves what it can](0185-a-control-plane-behind-its-seats-row-serves-what-it-can.md)
|
||||
- **0186** — [A ban list never holds a neighbour, and the mesh's own bans are its own wherever they hang](0186-a-ban-list-never-holds-a-neighbour.md)
|
||||
- **0187** — [A dead tracker is not the machine's failure](0187-a-dead-tracker-is-not-the-machines-failure.md)
|
||||
- **0190** — [A seat's work is shared by its holders, and building is the first such role](0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md)
|
||||
- **0188** — [A provider declares what it derives for each consumer, and the mesh tells both ends](0188-a-provider-declares-what-it-derives-for-each-consumer.md)
|
||||
- **0189** — [The store keeps what the records name, and a maintenance step holds its writers still](0189-the-store-keeps-what-the-records-name.md)
|
||||
|
||||
### Its tiers, from the bottom up
|
||||
|
||||
@@ -222,7 +223,6 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0126** — [A module declares its own seats; the mesh reserves its own](0126-a-module-declares-its-own-seats.md)
|
||||
- **0148** — [The mesh's names are resolved, not copied into every container](0148-the-meshs-names-are-resolved-not-copied-into-containers.md)
|
||||
- **0151** — [A route's internal name is composed under the node that serves it](0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md)
|
||||
- **0191** — [The mesh's resolver holds only the mesh's own names; a public name resolves publicly](0191-the-meshs-resolver-holds-only-the-meshs-own-names.md)
|
||||
|
||||
### What runs on them, and how it gets there
|
||||
|
||||
@@ -280,9 +280,6 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0150** — [A module's own code runs as supervised processes under the module's one account](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)
|
||||
- **0152** — [The operator's surface is a module the mesh assigns: the console](0152-the-operators-surface-is-a-module-the-console.md)
|
||||
- **0155** — [A definition names no installation: how that is checked, and the three ways a value that did gets out](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
|
||||
- **0164** — [A setting is declared with its default, its meaning and what changing it costs](0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md) *(proposed)*
|
||||
- **0165** — [`container-runtime` is what a machine can run; that a runtime is running is its holder's health](0165-container-runtime-is-what-a-machine-can-run-and-a-running-runtime-is-its-holders-health.md) *(proposed)*
|
||||
- **0166** — [The container runtime is a node seat, and the host creates containers through its holder](0166-the-container-runtime-is-a-node-seat-and-the-host-creates-containers-through-its-holder.md) *(proposed)*
|
||||
- **0173** — [The operator's machine is the mesh's, and a module is whatever it declares](0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md)
|
||||
- **0175** — [One tool runtime per node serves every module's tools, on the host side](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)
|
||||
- **0176** — [The login shell is a node seat held by one shell module, and `execute` is its contract](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)
|
||||
@@ -290,8 +287,6 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0181** — [The operator account is a node fact, and a home is a placement root](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)
|
||||
- **0182** — [Inside a home, the mesh owns the directory and the files it places, writes into the tool's own files, and holds everything else as found](0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
||||
- **0183** — [The Anthropic licence manager is a module holding a seat; it hands each node's agent its token over the bus, sealed; the controller and the host have no part](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
|
||||
- **0188** — [A module's own code is bundles in any language, and a tools bundle speaks MCP to the runtime](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)
|
||||
- **0192** — [A tools bundle declares what it is given, and the runtime hands it to that bundle alone](0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md)
|
||||
|
||||
### How it is built
|
||||
|
||||
|
||||
@@ -5,12 +5,10 @@ code:
|
||||
- mesh-controller internal/catalogue/filtering.go
|
||||
- mesh-controller examples/route-proxy
|
||||
- mesh-controller internal/identity/authority.go
|
||||
- mesh-controller cmd/mesh-controller/plan.go (the names the roster publishes)
|
||||
- mesh-host internal/identity/serving.go
|
||||
- mesh-host internal/apply (the service that reflects a rule set)
|
||||
updated: 2026-10-03
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 02-DECISIONS/0191-the-meshs-resolver-holds-only-the-meshs-own-names.md
|
||||
- 02-DECISIONS/0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md
|
||||
- 02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md
|
||||
- 02-DECISIONS/0169-a-machine-joins-through-the-tunnel-and-the-bus-is-never-public.md
|
||||
@@ -423,22 +421,7 @@ that module and nothing else.
|
||||
the argument for the table in ADR 0009 being a table: the pattern is only obvious once seen, and
|
||||
the cost of not seeing it is inventing a mechanism that already exists.
|
||||
|
||||
### The mesh resolves only its own names; a public name resolves publicly
|
||||
|
||||
**The mesh's resolver holds each node's internal domain and nothing else** — `<node>.internal` and
|
||||
everything under it, so every route's internal name `<label>.<node>.internal` with no line of its own
|
||||
([ADR 0151](../../02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md)).
|
||||
**A node's public domains — one or more — are never given a private answer**: it is forwarded and resolves to the
|
||||
public address, from a member and from anything else the resolver answers — a resolver may serve a
|
||||
machine's LAN, and a phone on that LAN must get the address it can reach
|
||||
([ADR 0191](../../02-DECISIONS/0191-the-meshs-resolver-holds-only-the-meshs-own-names.md)). Inside the
|
||||
mesh, a routed service is reached, and certified by the internal authority, under its internal name.
|
||||
*Checked by the controller's tests — the roster names the machines and no routed name —
|
||||
and on a machine by asking its resolver for a public name the mesh serves: the answer is the public
|
||||
address.*
|
||||
|
||||
*What follows is how the mesh got here, kept because the reasoning it rejects is the expensive half to
|
||||
rediscover.*
|
||||
### And the public names a proxy serves must resolve in the mesh too
|
||||
|
||||
*2026-09-09, found by an internal certificate authority that could not issue.* The mesh writes every
|
||||
`<node>.internal` name into every declared container and treats the public names a proxy serves as a
|
||||
@@ -461,13 +444,6 @@ would go stale the day one changes. The mesh propagates the names it was told to
|
||||
knows nothing about what they mean
|
||||
([ADR 0066](../../02-DECISIONS/0066-public-routing-is-name-agnostic.md)).
|
||||
|
||||
*2026-10-03, withdrawn.* Publishing public names with private answers turned every resolver that also
|
||||
serves a LAN into an outage for that LAN's non-members — a phone was handed the control-node's tunnel
|
||||
address for the mail server — while every check, run from a member, passed. Its reason had gone: routes
|
||||
have internal names since ADR 0151, and the proxy certifies public names from a public authority and
|
||||
internal names from the internal one. Superseded by the rule at the head of this section
|
||||
([ADR 0191](../../02-DECISIONS/0191-the-meshs-resolver-holds-only-the-meshs-own-names.md)).
|
||||
|
||||
## 3 — Exposure
|
||||
|
||||
Settled by [ADR 0007](../../02-DECISIONS/0007-connectivity.md); summarised here because
|
||||
@@ -895,15 +871,13 @@ is unchanged. **The same code path certifies against an internal authority as ag
|
||||
only the issuer differs.** That is what makes trusted certificates possible for a mesh whose names
|
||||
the public internet cannot resolve.
|
||||
|
||||
**The internal authority certifies internal names; a public one certifies public names.** The
|
||||
authority's challenge reaches the name it certifies, so each certifies what it can resolve: the
|
||||
internal authority a route's `<label>.<node>.internal`, which the mesh resolves, and a public authority
|
||||
the public name, which public DNS resolves. A proxy holds both, and a public name is never certified
|
||||
by the internal authority. *Checked by a handshake to a route's internal name that verifies against
|
||||
the internal root and nothing else, and one to its public name that verifies against the public
|
||||
roots* ([ADR 0191](../../02-DECISIONS/0191-the-meshs-resolver-holds-only-the-meshs-own-names.md)).
|
||||
Until 2026-10-03 this paragraph had the internal authority certify public names, which needed them
|
||||
resolved inside the mesh ([ADR 0066](../../02-DECISIONS/0066-public-routing-is-name-agnostic.md)).
|
||||
**And it does not work until the routed name resolves inside the mesh** — the §2 finding above,
|
||||
arriving here because this is what needed it. The authority's challenge reaches the routed name only
|
||||
once that name is in internal resolution; a public authority is handed that dependency by public
|
||||
DNS, and an internal one has to be handed it by the mesh. *Checked by a handshake to a routed name
|
||||
that verifies against the internal root and nothing else — which cannot succeed unless the issuer
|
||||
first reached the name to certify it*
|
||||
([ADR 0066](../../02-DECISIONS/0066-public-routing-is-name-agnostic.md)).
|
||||
|
||||
## 6 — One statement behind exposure, filtering and certificates
|
||||
|
||||
@@ -1009,6 +983,10 @@ The list is worth having in one place, because it is most of the argument:
|
||||
operator's to move between meshes, but the manifest layer still stores it as a literal — so today
|
||||
the composition is a per-node override rather than the design. The interpolation that would let a
|
||||
module carry a label and a node carry the domain, and the mesh join them, does not yet exist.
|
||||
- **Publishing route names into internal resolution.** The same ADR requires a granted route to be
|
||||
resolvable inside the mesh, not only routable from outside it; the mechanism that writes
|
||||
`<node>.internal` into containers does not yet also write the routed names, which is why an
|
||||
internal issuer cannot currently validate one without a hand-placed entry.
|
||||
|
||||
## The hub adopts the predecessor's tunnel
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ status: proposed
|
||||
code:
|
||||
- mesh-controller cmd/mesh-builder
|
||||
- mesh-controller internal/builder
|
||||
- mesh-catalog modules/build-agent
|
||||
updated: 2026-10-03
|
||||
- mesh-catalog modules/builder
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 02-DECISIONS/0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md
|
||||
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||
- 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md
|
||||
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
||||
- 02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md
|
||||
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
|
||||
@@ -269,29 +269,6 @@ ships one and wrong for code the mesh built, which has no unit until the mesh wr
|
||||
**Tools, hooks and consumers are not further modes**, which is the test of whether three is the
|
||||
right number: they are loaded by a tool host, and a tool host is a process that stays up.
|
||||
|
||||
## Where a build runs
|
||||
|
||||
**On whichever machine holding the build role is idle** ([ADR 0190](../../02-DECISIONS/0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md)).
|
||||
The role is `node-build-agent`, a node seat; its holder is the `build-agent` module, assignable to
|
||||
every machine with a container runtime. The controller asks the role, never a machine: a tier's asks go
|
||||
onto the seat's one work queue together, and each holder pulls one at a time when it is idle, so a
|
||||
tier of many images is built by as many machines as hold the seat and are online, and a machine that
|
||||
is off builds nothing and blocks nothing. What a holding machine needs is what the builder always
|
||||
needed — a container runtime, the artifact store and the package registry as provisions, a workspace,
|
||||
the bus credential — said once in the module's manifest. The outcome names the machine that built it.
|
||||
|
||||
This is the bus's shared-work pattern, not a build-specific one: any module declaring a node seat with
|
||||
`accepts` has its work shared by its holders the same way. Building is the first use.
|
||||
|
||||
*Built and proven live 2026-10-03.* `build-agent` holds `node-build-agent` on all four machines; the first
|
||||
build taken by a workstation's agent was a catalogue module at 09:35 UTC; the one-holder `builder` is
|
||||
retired. The switch found five gaps, each an issue: a worker whose type changed stranded its holder
|
||||
([206](../../04-ISSUES/206-a-seats-worker-changing-type-strands-the-holder-and-the-build-that-would-fix-it/00-report.md)),
|
||||
a re-made worker replayed the stream's history ([207](../../04-ISSUES/207-a-re-made-worker-replayed-every-ask-the-stream-kept/00-report.md)),
|
||||
a seat's worker is made only when the controller starts ([208](../../04-ISSUES/208-a-seats-worker-is-made-only-when-the-controller-starts/00-report.md)),
|
||||
a module's identifier must fit the tightest backend's key (the slug), and an idle machine's empty fetch
|
||||
was read as the end (fixed in the controller the same day).
|
||||
|
||||
## A build says what it does, as it happens
|
||||
|
||||
*2026-10-01 — [ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md).*
|
||||
@@ -317,6 +294,44 @@ build's lines reach a reader of its subject in order and the stream holds them a
|
||||
against a real server); the seat verb with an id reads the log (controller test); and, live, a build
|
||||
after the roll-out read line by line through the console.
|
||||
|
||||
## The store keeps what the records name
|
||||
|
||||
*2026-10-02 — [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md),
|
||||
[issue 108](../../04-ISSUES/108-the-registry-has-no-garbage-collection-once-it-has-two-doors/00-report.md).*
|
||||
|
||||
Every build pushes another layer set and, until this, nothing ever removed one. The registry's own
|
||||
answer — collect what no tag names — is wrong for this mesh: each artifact is pushed under one
|
||||
moving tag and machines are pinned by digest, so every build but the newest is untagged and some
|
||||
machine may still be running it.
|
||||
|
||||
**The mesh decides and the store reclaims.** Deletion is enabled on the store's one door — that
|
||||
door already accepts a push, and a writer who can push can replace any tag, so delete takes
|
||||
nothing a push did not already have, and ADR 0082's bargain (a store every machine reaches with no
|
||||
credential to distribute first) is kept. The mesh then removes what it put there and no longer
|
||||
keeps, **naming it from its own build records** rather than enumerating the store: it has never
|
||||
put anything there it did not record, so a digest it did not record making is never named, which
|
||||
is what keeps the sweep away from the images genesis pushed before any record existed.
|
||||
|
||||
An artifact stays for one of two reasons and otherwise goes: a definition the mesh holds names it
|
||||
(no age limit — this is the floor), or it belongs to one of the five most recent successful builds
|
||||
of its module (somewhere for a wrong release to return to). The sweep runs after a build the mesh
|
||||
recorded, which is the moment new bytes landed and the moment the keep set moved; it needs no
|
||||
timer. Deleting a manifest frees no bytes, so the store's own collector runs nightly as a
|
||||
scheduled step with the server held still — which is what `while-stopped` exists for
|
||||
([design 20](20-writing-a-module.md)). Plain collection, not `--delete-untagged`: what the mesh
|
||||
keeps is still a manifest and so still referenced, and the dangerous flag is not needed once the
|
||||
mesh is the one deciding.
|
||||
|
||||
A machine behind by more than five builds of a module, recreating a container, cannot pull what it
|
||||
was running. It is already a machine the mesh reports as behind, and the answer is the current
|
||||
declaration.
|
||||
|
||||
*How it is checked:* the keep set, against records, holds what a manifest names and the five most
|
||||
recent builds and nothing else; a reference the mesh never recorded is never in the delete set; an
|
||||
image and an archive are asked for at their own endpoints; a store with deletion off names the
|
||||
remedy rather than the status code; a store that does not have it is recorded collected rather
|
||||
than retried for ever. Live: the store's size before and after the first nightly collection.
|
||||
|
||||
## The builder compiles the languages the mesh is written in
|
||||
|
||||
*2026-09-29 —
|
||||
|
||||
@@ -5,11 +5,12 @@ code:
|
||||
- mesh-catalog modules/showcase
|
||||
- mesh-controller internal/builder
|
||||
- mesh-sdk src
|
||||
updated: 2026-09-30
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
||||
- 02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md
|
||||
- 02-DECISIONS/0053-a-step-that-runs-on-a-schedule.md
|
||||
- 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md
|
||||
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
|
||||
- 02-DECISIONS/0040-what-a-module-is.md
|
||||
- 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
|
||||
@@ -208,3 +209,27 @@ is recreated with the new fact
|
||||
([ADR 0099](../../02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md)). *How it is
|
||||
checked:* the host's unit tests run a step again when its named file changed and not otherwise,
|
||||
and recreate a container naming a step after the step ran.
|
||||
|
||||
## A recurring step may hold its own module's containers still
|
||||
|
||||
*Written 2026-10-02, from [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md)
|
||||
and [issue 108](../../04-ISSUES/108-the-registry-has-no-garbage-collection-once-it-has-two-doors/00-report.md).*
|
||||
|
||||
Some work cannot be done underneath a running service: an artifact store's collector walks the
|
||||
storage and requires every writer stopped. A `run-once` step runs *beside* containers and a
|
||||
scheduled one is the same container again, so until this a module had no way to say it — and the
|
||||
mesh inherited a store that has never collected anything, because the predecessor said it with a
|
||||
shell script and a script beside a module is not a resource in it.
|
||||
|
||||
A scheduled step may name `while-stopped`: resource ids of **its own module's** containers, which
|
||||
the host stops before the run and starts again after it, in the reverse order, **whatever the step
|
||||
did**. Three boundaries, each refused where it can be seen earliest — its own module's containers
|
||||
only, because a module that could quiesce a neighbour could stop the mesh; scheduled steps only,
|
||||
because at apply the declaration is applied in order and a step already gates what follows, so a
|
||||
one-time offline job says *before* rather than *instead of*; and restoring that is not conditional
|
||||
on anything, because the only real risk of the field is a window that never closes.
|
||||
|
||||
*How it is checked:* the host's unit tests assert stop–run–start in that order, the restart after a
|
||||
step that **failed**, the reverse order for several containers, and a service left down said
|
||||
loudly. The controller refuses, from the definition alone, a window with no schedule, one on a
|
||||
run-once step, one naming a container the module does not declare, and one naming itself.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-controller internal/catalogue]
|
||||
updated: 2026-09-30
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||
- 02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md
|
||||
@@ -14,6 +14,7 @@ decisions:
|
||||
- 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
|
||||
- 02-DECISIONS/0188-a-provider-declares-what-it-derives-for-each-consumer.md
|
||||
---
|
||||
|
||||
# 27 — A module requires, the mesh resolves
|
||||
@@ -206,6 +207,22 @@ name when nothing sets it. That is the contract half of this design's operator p
|
||||
the placeholder allows: the definition says which values reach which requirement, and nothing else
|
||||
does. *How it is checked:* the unit tests named in issue 173, and the plan comparison that closed it.
|
||||
|
||||
*A provider says once what it derives for each consumer (2026-10-02,
|
||||
[ADR 0188](../../02-DECISIONS/0188-a-provider-declares-what-it-derives-for-each-consumer.md),
|
||||
[issue 124](../../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md)):*
|
||||
where a provider **names the resource** it gives each consumer — a bucket, a database, a vhost — the
|
||||
name is derived per consumer, and a literal `serves` block could not carry it. A served value may
|
||||
now name the consumer the mesh is serving: `${consumer:as}`, the identity the mesh minted, and
|
||||
`${consumer:as:dns}`, that same identity written as a DNS label. Nothing else — **the mesh learns no
|
||||
protocol here; it spells its own name in an alphabet it already knows.** Settings are laid on first,
|
||||
so an operator may still set a prefix and the mesh derives the rest. The mesh fills it at the one
|
||||
moment it knows who the consumer is, and the one filled value reaches both ends: the consumer, as
|
||||
its binding's served facts and as `${bound:<provision>:<key>}` in any file it writes; the provider,
|
||||
as `derived` on that consumer's entry in its contributions file, so its provisioner is told the name
|
||||
rather than recomputing it. A consumer that writes the derived value into its own definition instead
|
||||
of asking for it is refused, naming the placeholder to use. *How it is checked:* the unit tests in
|
||||
ADR 0188's "how this is checked", each run against the unchanged controller first.
|
||||
|
||||
## 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
|
||||
@@ -214,7 +231,9 @@ name in a configuration file writes the same thing: the requirement's name and t
|
||||
controller fills it at resolution.
|
||||
|
||||
This one form replaces the placeholders that exist today, one per mechanism: bound values, secrets,
|
||||
ports and machine facts.
|
||||
ports and machine facts. It subsumes the consumer placeholder too — a value a provider derives is
|
||||
read by the consumer exactly as any other field of the contract is, and `${consumer:…}` is only
|
||||
how the *provider* states the rule.
|
||||
|
||||
**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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-tools, mesh-controller, mesh-host, mesh-catalog]
|
||||
updated: 2026-10-03
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
|
||||
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
|
||||
@@ -11,8 +11,6 @@ decisions:
|
||||
- 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
|
||||
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||
- 02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md
|
||||
- 02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md
|
||||
- 02-DECISIONS/0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md
|
||||
---
|
||||
|
||||
# 38. Building the operator's machine
|
||||
@@ -99,19 +97,6 @@ what `tools` answers, and the others serve. The runtime reads `MESH_OPERATOR_ACC
|
||||
five tools and two seat verbs answer on their subjects; `tools` names the failed bundle; a
|
||||
membership republished mid-run re-subscribes without a restart.
|
||||
|
||||
*Built and proven 2026-10-02* (mesh-tools, branch `feat/the-operators-machine`, commit `6390d1d`).
|
||||
|
||||
**WP1b — the launcher beside the loader** ([ADR 0188](../../02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)).
|
||||
*mesh-tools, mesh-sdk. A day for the skeleton.* A bundle whose entry is not JavaScript is launched
|
||||
as a child process with the runtime's environment and spoken to over MCP on stdio: `tools/list`
|
||||
once, `tools/call` per call; a tool named `<seat>.<verb>` is the seat's implementation. A child
|
||||
that exits is named as a failed bundle and restarted on the next call. The TypeScript import stays
|
||||
as the shortcut. Beside it, one skeleton SDK per language of the first set — the stdio loop and the
|
||||
tool-definition type, nothing else — each proven by one bundle in that language answering one tool
|
||||
in the runtime's test. **Proof.** The runtime's test: a bundle in a second language, launched, its
|
||||
tool answering on its subject over a real bus; the TypeScript fixture served through the protocol
|
||||
with the shortcut off answers the same.
|
||||
|
||||
## WP2 — The controller composes one runtime per node
|
||||
|
||||
*mesh-controller. Two to three days; the largest package.*
|
||||
@@ -126,23 +111,12 @@ with the shortcut off answers the same.
|
||||
declaration gains an `archive` placed under a directory the controller derives, so the host
|
||||
fetches and unpacks it as it does any artifact. The bundle's digest is what the build recorded.
|
||||
3. **The runtime's process.** One `process` per node running the runtime from its own bundle
|
||||
(WP3), `MESH_TOOL_MODULES` composed from the unpacked entrypoints — each as
|
||||
`<module>=<path>`, and the runtime decides from the file whether it is loaded or launched
|
||||
(WP1b) — `MESH_OPERATOR_ACCOUNT` and
|
||||
(WP3), `MESH_TOOL_MODULES` composed from the unpacked entrypoints, `MESH_OPERATOR_ACCOUNT` and
|
||||
`MESH_OPERATOR_HOME` from the account fact, `restart-on` naming every bundle so a push that
|
||||
changes one restarts it. A node with no account composes the runtime without the two words.
|
||||
4. **The gate.** A manifest declaring `tools` and a container built on the runtime's base image is
|
||||
refused at registration once the runtime module is registered, naming this record. It is the
|
||||
mechanism that keeps the old pattern from returning by habit. ADR 0188 widens it, after WP4:
|
||||
a module whose own code is an image artifact is refused, whatever image it is built on.
|
||||
|
||||
*Amended 2026-10-02, at WP3.* The gate refuses the pattern **spreading**, not standing: a module
|
||||
new to the catalogue in that shape, or one that had already moved to a bundle and returns to it,
|
||||
is refused; a module the catalogue already holds in that shape — judged from the manifest it
|
||||
holds and what that module's newest build stood on — is rebuilt without complaint. The day the
|
||||
runtime arrives some thirty such modules stand, each moves in its own change from WP4 on, and a
|
||||
gate refusing every rebuild in the meantime would stop the catalogue's pipeline to make a point
|
||||
this record already makes.
|
||||
mechanism that keeps the old pattern from returning by habit.
|
||||
|
||||
**Proof.** Composition tests: a node with three assigned modules, one holding a seat, yields one
|
||||
process, three archives, one node principal whose grants are the union, and the same three
|
||||
@@ -164,22 +138,6 @@ runtime's `serve` keeps answering MCP on loopback; the person's end of it keeps
|
||||
wrote, `tools/list` on loopback answers as before, and the controller's verbs answer through it.
|
||||
This is the first live step, and it is reversible by re-assigning `mesh-console`.
|
||||
|
||||
*Decided 2026-10-02:* `mesh-tools` keeps its name as the module the TypeScript images come from, and
|
||||
`node-tools` is a second module in the same repository ([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md))
|
||||
rather than a rename — the thirty-five manifests that build `on` `mesh-tools` stay true. Three things
|
||||
WP3 found that the plan did not say: a TypeScript bundle must carry its dependencies and a
|
||||
`package.json` naming its files as ES modules, which the toolchain now copies in from its own image;
|
||||
the runtime's credential must be owned by the account the runtime runs as, which the controller
|
||||
composes; and `MESH_TOOL_MODULES` is empty on a node where the runtime is the only bundle, which the
|
||||
runtime accepts. *Built 2026-10-02* (mesh-tools `c46f950`, mesh-controller `ca7e81e` `773b561`
|
||||
`729a5f9`). *Proven live 2026-10-02/03, on all four machines*: the console's container is gone,
|
||||
`node-tools` runs as a unit the host wrote, as the operator's account, `tools/list` on each loopback
|
||||
answers with the same 219 tools as before, and the controller's verbs answer through it; `mesh-console`
|
||||
retired from the catalogue. Three things the step found are issues
|
||||
[203](../../04-ISSUES/203-a-fresh-assignment-is-pushed-before-its-credential-exists/00-report.md),
|
||||
[204](../../04-ISSUES/204-a-controller-handover-re-sent-every-node-a-stale-declaration/00-report.md) and
|
||||
[205](../../04-ISSUES/205-a-package-resource-fails-against-a-stale-package-database/00-report.md).
|
||||
|
||||
## WP4 — The first holder moves: the packet filter
|
||||
|
||||
*mesh-catalog. Half a day. The live proof of ADR 0175.*
|
||||
@@ -192,78 +150,6 @@ tool where they need root, which they have, since the runtime runs as the node's
|
||||
four machines; `docker ps` shows no `mesh-nftables`; `status` is well. Then the fail2ban holder
|
||||
proposed in an open change follows the same way when it lands.
|
||||
|
||||
*Found 2026-10-03, before the step ran:* a bundle imported in-process brings its own copy of the SDK
|
||||
(WP3's *carry its dependencies*), and the SDK's tool registry is the copy's own — the first module
|
||||
loaded beside the runtime would have registered its tools where the runtime never looks, and served
|
||||
nothing, silently. Issue
|
||||
[209](../../04-ISSUES/209-a-bundles-own-sdk-copy-registers-into-a-registry-the-runtime-never-reads/00-report.md):
|
||||
the runtime now resolves every bundle's import of the SDK to its own copy, one registry and one
|
||||
broker per node. Two things the package did not say, settled in the module: the filter's commands
|
||||
run through `sudo` without a prompt where the runtime is not root, since the operator's account may
|
||||
escalate as the operator would; and a bundle has no environment of its own, so the tool reads the
|
||||
filter from the path the manifest's `filtering` names rather than from a variable the container used
|
||||
to carry, a test holding the two together. Three things a review of the change found: the module's
|
||||
own bus credential and state directory went with the container, since nothing reads them once the
|
||||
runtime speaks with the node's (the shell module of WP5 declares neither); the `iptables` package the
|
||||
image used to carry is now declared on the host; and that the operator's account may escalate without
|
||||
a prompt is a fact about the machine the mesh neither declares nor checks — true on all four today,
|
||||
and when it is not, the tool names it by how it failed, which is the only check there is until a
|
||||
record says where the fact belongs.
|
||||
*Built 2026-10-03* (mesh-tools `7152148` for issue 209, mesh-catalog `db5e7c8`). *Proven live
|
||||
2026-10-03, on all four machines*: `node-packet-filter.rules`, `reload` and `remove` answer from the
|
||||
node's runtime on each — `rules` and the module's own tool list the mesh's table, `reload` loads the
|
||||
file and answers with the table, `remove` refuses the mesh's own table by name — `docker ps` shows no
|
||||
`mesh-nftables` on any, the container's credential is gone with it, and `status` is well. One thing
|
||||
the step found is issue
|
||||
[210](../../04-ISSUES/210-the-host-re-creates-the-nodes-runtime-on-every-reconcile/00-report.md):
|
||||
the host re-creates the runtime's process on every reconcile (resolved the same day, mesh-host #80).
|
||||
*fail2ban followed 2026-10-03* (mesh-catalog `aa5bf7d`), the same shape: container, base images,
|
||||
credential and state directory gone, the client through `sudo` since the daemon's socket is root's;
|
||||
proven on all four machines — `status`, `banned` and the module's own `fail2ban_settings` answer from
|
||||
the runtime, no `mesh-fail2ban` container, the runtime serving both bundles. Two holders moved; of the
|
||||
thirty-three tool containers the catalogue held, thirty-one remain, and all but these two carried their
|
||||
module's configuration and secrets in the container's environment, which a bundle does not have — the
|
||||
question research [020](../../01-RESEARCH/020-what-a-bundled-tool-is-given/00-overview.md) opened
|
||||
and [ADR 0192](../../02-DECISIONS/0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md)
|
||||
settled the same day: a tools bundle declares `env` on its artifact, the composer resolves it as a
|
||||
container's, the runtime hands each bundle its own. That is WP4b below.
|
||||
|
||||
## WP4b — Every tool container moves
|
||||
|
||||
*mesh-controller, mesh-tools, mesh-catalog. One day. The rest of ADR 0175, under ADR 0192.*
|
||||
|
||||
**What changes**, in order: the manifest's tools artifact gains `env` and the catalogue check
|
||||
refuses a secret's content in it; the composer resolves a bundle's `env` per machine and carries it
|
||||
beside the bundle's archive, `restart-on` included; the runtime hands each bundle its own
|
||||
environment — the contributor's argument for an imported bundle, the child's environment for a
|
||||
launched one — and a test holds two bundles apart. Then the thirty-one remaining tool containers
|
||||
move in one change: each container's `env` becomes its tools artifact's, mount targets folded into
|
||||
the host paths they came from, the container, its base images, its Dockerfile and its own bus
|
||||
credential gone. Last, the registration gate refuses the container shape for every module.
|
||||
|
||||
**Proof.** The controller's and the runtime's tests named in ADR 0192; live, every module's tools
|
||||
answer from the runtime on the machines that run it, `docker ps` shows no tool container on any of
|
||||
the four, and `status` is well.
|
||||
|
||||
*Found 2026-10-03, building it:* of the thirty-one, nine run only tools, three a main of their own,
|
||||
and twenty import the module's own event handlers and provisioners beside their tools (ADR 0192's
|
||||
dated note). WP4b moves the tools-only nine; WP4c holds the rest. Two of the nine stay with WP4c as
|
||||
well — one carries a run-once provisioning step in a second container, one reads an env-file and two
|
||||
sockets — so seven move here. *Built 2026-10-03:* mesh-sdk #12 (`collectToolsEach`), mesh-tools #33
|
||||
(each bundle its own environment), mesh-controller #236 and #237 (the words composed, made the
|
||||
account's to read, and named files restarting the runtime), and the seven modules in one change.
|
||||
Building it found issue [211](../../04-ISSUES/211-a-bundle-is-built-before-the-toolchain-it-is-compiled-in/00-report.md).
|
||||
|
||||
## WP4c — The module's own long-running code moves
|
||||
|
||||
*Not yet broken down.* Twenty-three containers carry code that is not a tool: event handlers,
|
||||
provisioners, a step, a main. [ADR 0188](../../02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)
|
||||
§3 already says such code is a `process` bundle the host runs. What no record says yet is how that
|
||||
process is given what its container was: the module's own bus credential and the subscriptions it
|
||||
consumes with, the words its code reads at import, the packages the image installed (a database's
|
||||
client), and the service it reaches by a container network name. That begins with a decision record,
|
||||
after which the twenty-three move and the registration gate refuses the container shape for all.
|
||||
|
||||
## WP5 — The shell, on a server first
|
||||
|
||||
*mesh-catalog #224, already written. Half a day to assign and prove.*
|
||||
|
||||
+33
-4
@@ -1,9 +1,9 @@
|
||||
---
|
||||
status: open
|
||||
status: resolved
|
||||
opened: 2026-09-23
|
||||
located-in: []
|
||||
fixed-by:
|
||||
amended-design:
|
||||
located-in: [mesh-controller internal/inventory, mesh-controller internal/artifacts, mesh-host internal/apply, mesh-catalog modules/distribution]
|
||||
fixed-by: 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md
|
||||
amended-design: 03-DESIGN/01-to-be/18-building-a-module.md
|
||||
---
|
||||
|
||||
# 108 — The registry has no garbage collection, and two doors make it harder to add
|
||||
@@ -75,3 +75,32 @@ real thing services need, and the mesh cannot express one.
|
||||
images by digest and moves by version — is retention "the digests no recorded build names"?
|
||||
- Who owns the routine when the store and its public door are two modules — the store, since the
|
||||
volume is its?
|
||||
|
||||
## Answered, 2026-10-02 — [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md)
|
||||
|
||||
The three open questions, answered:
|
||||
|
||||
- **A maintenance step, or a backend that does not need its writers stopped?** The step. A
|
||||
scheduled container may name `while-stopped` — resource ids of **its own module's** containers,
|
||||
which the host stops before the run and starts again after it whatever the step did. A storage
|
||||
backend the mesh does not run would be a bigger thing to own than the mechanism it avoids, and
|
||||
the mechanism is wanted anyway: a service that cannot have work done underneath it is a real
|
||||
shape and the mesh could not express it at all.
|
||||
- **Is retention "the digests no recorded build names"?** Nearly. An artifact stays because a
|
||||
definition the mesh holds names it (no age limit), or because it belongs to one of the five most
|
||||
recent successful builds of its module. Last-N-tags was the predecessor's rule for a registry
|
||||
that knew nothing else; this mesh knows what each digest is for.
|
||||
- **Who owns the routine now the second door is gone?** Both halves, each where it can be. The
|
||||
**mesh** decides what may go — only it holds the records — and asks the store to drop it. The
|
||||
**store** reclaims the bytes, because only it can stop its own server. Neither half can be done
|
||||
by the other.
|
||||
|
||||
And the sharpened point — enabling deletion on a door with no accounts — dissolved on inspection:
|
||||
**that door already accepts a push**, so a writer who can reach it can already replace any tag.
|
||||
Delete takes nothing a push did not have. What it does not do is undo ADR 0082's bargain, which
|
||||
putting an authenticated door in front of deletion would have.
|
||||
|
||||
The second registry process is not built, as the 2026-09-26 note says, so the shared blob cache
|
||||
and the deletion-cached-by-the-other-door problem never arise. Plain `garbage-collect` is enough:
|
||||
what the mesh keeps is still a manifest in the store, so `--delete-untagged` — the flag that would
|
||||
delete images machines are running — is not needed at all.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-09-26
|
||||
located-in: [mesh-controller internal/catalogue/declaration.go, mesh-sdk src/provisioner, mesh-catalog modules/minio]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
located-in: [mesh-controller internal/catalogue, mesh-sdk src/provisioner, mesh-catalog modules/minio]
|
||||
fixed-by: 02-DECISIONS/0188-a-provider-declares-what-it-derives-for-each-consumer.md
|
||||
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||
---
|
||||
|
||||
# 124 — A consumer cannot be told a value its provider derived for it, so it transcribes one
|
||||
@@ -63,3 +63,27 @@ compares it to what the provider will actually create. The one wrong instance wa
|
||||
- What would have caught the wrong instance? A test that resolves a consumer's grant and compares the
|
||||
bucket in its own configuration against the one the provider would create is a check that could
|
||||
exist today, for any interface, without the mechanism above.
|
||||
|
||||
## Answered, 2026-10-02 — [ADR 0188](../../02-DECISIONS/0188-a-provider-declares-what-it-derives-for-each-consumer.md)
|
||||
|
||||
The channel is the provider's own `serves` block, which may now name the consumer the mesh is
|
||||
serving: `${consumer:as}` and `${consumer:as:dns}`. The mesh fills it once, where it knows who the
|
||||
consumer is, and delivers the one filled value to both ends — the consumer's binding and its
|
||||
`${bound:…}` substitutions, and the provider's contributions entry, so a provisioner is told the
|
||||
name rather than deriving it. Each open question above, answered:
|
||||
|
||||
- **Should a provider return values from provisioning?** No. It would make a grant carry data the
|
||||
provider wrote, make a consumer's declaration wait on its provider's reconcile loop, and put the
|
||||
rule where nothing can refuse it. The reasoning is in the record.
|
||||
- **Or should `serves` say a value is derived?** Yes, and the mesh performs the derivation — but it
|
||||
learns no protocol doing it. The only fact is the identity the mesh itself minted, in one of two
|
||||
alphabets it already knows.
|
||||
- **Should a consumer that names the resource be refused?** Yes. A consumer's file that already
|
||||
contains the value the mesh is about to derive for it is refused at resolution, naming the
|
||||
placeholder to write instead. That is the check this report asked for, and it is exact rather than
|
||||
heuristic: a derived value carries the identity minted for this consumer on this machine, which
|
||||
nothing else would spell out.
|
||||
|
||||
minio's `bucketFor` is gone; its manifest serves `"bucket": "${consumer:as:dns}"`. The three
|
||||
consumers' hand-written bucket names are gone with it — each of them also named the machine the
|
||||
module happens to run on, which is the second thing wrong with a transcription.
|
||||
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
---
|
||||
status: located
|
||||
opened: 2026-10-01
|
||||
located-in: [mesh-catalog modules/dnsmasq, mesh-controller internal/overlay/generator.go, mesh-controller internal/catalogue/resolve.go (checkResources)]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 190 — The container runtime's configuration is written by modules that are not the runtime's
|
||||
|
||||
## What was observed
|
||||
|
||||
The runtime's configuration file and its service are declared by two parties, neither of which is
|
||||
the runtime:
|
||||
|
||||
- **The resolver module** writes the runtime's `dns` key (the machine's private address) and
|
||||
`live-restore` into the runtime's file, written into rather than over
|
||||
([ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md)). It also
|
||||
declares the runtime's service, reloaded when that file changes. The `dns` key has been written
|
||||
since the resolver module was converted from its predecessor on 2026-09-23; `live-restore` and the
|
||||
service were added on 2026-09-30 while fixing
|
||||
[issue 110](../110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md),
|
||||
where containers silently resolved through a public resolver.
|
||||
- **The private network** writes the runtime's `insecure-registries` into the same file, and declares
|
||||
the same service reloaded on it, as [ADR 0082](../../02-DECISIONS/0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md)
|
||||
and ADR 0102 decided. The controller generates both resources per machine.
|
||||
|
||||
On the three machines that run the resolver module, both declare one path and one unit. Nothing refuses
|
||||
it. The collision check compares the resources of catalogue modules. The private network is computed,
|
||||
so its resources are produced when a machine's declaration is composed, and the check never sees them.
|
||||
|
||||
The machine without the resolver module shows the other half. Its runtime still has the predecessor's
|
||||
resolver and `live-restore` off, because the only module that sets them is a DNS server. A machine
|
||||
gets a correct container runtime only as a side effect of being given a resolver.
|
||||
|
||||
> **Later the same day, 2026-10-02.** The resolver module and its sibling for the resolver file were
|
||||
> assigned to the fourth machine ([issue 198](../198-the-lans-dns-server-ran-outside-the-mesh-and-its-filter-closed-it/00-report.md)),
|
||||
> so all four now have the resolver writing into the runtime's file, and the predecessor's
|
||||
> `live-restore: false` there is gone. The same work made the runtime's file, as the resolver declares
|
||||
> it, take no settings: a setting meant for the resolver's own configuration had reached it. The
|
||||
> collision and the ownership question above are unchanged.
|
||||
|
||||
## Why this is here
|
||||
|
||||
The operator ruled it a defect, not a design: **a module does not write another software's
|
||||
configuration.** The need behind each write is real. Containers must resolve the mesh's names
|
||||
([ADR 0148](../../02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md) step 2).
|
||||
A daemon restart must not stop every container. Every machine on the network must trust the mesh's
|
||||
registry. But each of these is a fact the runtime must be *given*, and the module that gives it is the
|
||||
runtime's own. With three writers, nobody can say what the file should contain. Two of the facts are
|
||||
reloaded when one of them needs a restart (issue 110's first fault). And the moment a module for the
|
||||
runtime exists, it is refused on every machine with the resolver, or, through the private network's
|
||||
path, accepted without anyone noticing a collision.
|
||||
|
||||
## What resolves it
|
||||
|
||||
[ADR 0166](../../02-DECISIONS/0166-the-container-runtime-is-a-node-seat-and-the-host-creates-containers-through-its-holder.md)
|
||||
gives the runtime a module that holds its seat and owns its file and service.
|
||||
[ADR 0164](../../02-DECISIONS/0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md)
|
||||
gives that module declared settings with defaults. The fix, once both are accepted:
|
||||
|
||||
1. The resolver module drops its runtime file and runtime service. It knows nothing of the runtime.
|
||||
2. The private network stops generating either resource. ADR 0082's decision stands — being on the
|
||||
network is what grants the trust, and no module author is involved — and only *who writes it*
|
||||
moves. The mesh gives the registry to the runtime module as a value. ADR 0082 and ADR 0102 each
|
||||
get a dated note saying where their mechanism now lives.
|
||||
3. The runtime module writes `dns`, `live-restore` and `insecure-registries`, each a declared
|
||||
setting with its cost: `dns` costs a restart, which `live-restore` makes harmless.
|
||||
4. Steps 1–3 land in one push. A runtime module declaring the file beside a resolver module still
|
||||
declaring it is refused.
|
||||
5. The collision check sees a computed module's resources as well, so a second writer cannot come
|
||||
back through generated code.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **How the resolver's address reaches the runtime.** Either the resolver seat (`node-dns-resolver`)
|
||||
delivers an address its holder serves, or the runtime module reads a machine fact and the seat
|
||||
being held is only a precondition. The first tracks a resolver moving off the private address. The
|
||||
second needs nothing new.
|
||||
- **What `dns` defaults to on a machine with no resolver seat held.** Nothing, leaving the runtime's
|
||||
own behaviour, is the honest default. A public resolver hides exactly the failure issue 110 took a
|
||||
day to find.
|
||||
- **The adopted machine's predecessor values.** The runtime module adopting a file with a
|
||||
hand-written `dns` and `live-restore: false` replaces both. That is intended, and is the one
|
||||
restart the operator must make on that machine.
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-10-02
|
||||
located-in: [mesh-catalog modules/mesh-console, mesh-controller cmd/mesh-controller/plan.go (port assignment)]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 192 — The mesh's tools reach a person only by a registration made by hand
|
||||
|
||||
## What was observed
|
||||
|
||||
A design session on a workstation had none of the mesh's tools. The console was running on that
|
||||
machine and answering on its loopback port. It was reached over the bus as the console's account, and
|
||||
listed every running module's tools and every seat's verbs
|
||||
([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)). What was missing
|
||||
was the registration that tells the person's coding agent where the console is. That registration had
|
||||
been made by hand, once, while migrating the machine, and scoped to the one project directory it was
|
||||
made in. Every session started anywhere else had no mesh tools. Nothing said so: the agent simply
|
||||
offered no mesh tools, and the session fell back to a pull-request link for a person to open by hand.
|
||||
|
||||
The predecessor did this job itself: it wrote its tool server into the agent's user configuration on
|
||||
every machine. Migrating removed that entry, as it should have, and no module took the job over.
|
||||
|
||||
## Why this is here
|
||||
|
||||
Three gaps, each of which would have stopped a module from doing it even if one existed.
|
||||
|
||||
**1. The console tells nobody where it is.** Its definition listens on a port and provides nothing.
|
||||
A module that wanted to point an agent at the console has no requirement it could name, so it
|
||||
would have to write the address into its own definition as a literal. That is exactly what
|
||||
[ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) and
|
||||
[ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)
|
||||
remove.
|
||||
|
||||
**2. The console's port is one its definition chose.** The definition names a port, and the mesh
|
||||
never assigned one: no port assignment exists for the console on any machine. The plan assigns a
|
||||
machine port only to a port a container publishes through a mapping, "without one the software binds
|
||||
what it binds". The console runs on the host network with no mapping, but it reads its listening
|
||||
address from `${port:…}`, so the mesh could move it and does not. That is a module choosing a
|
||||
machine port, which [ADR 0038](../../02-DECISIONS/0038-the-mesh-assigns-the-port.md) exists to
|
||||
prevent, through a gap in how the rule is applied rather than a decision against it. A module that
|
||||
reads its port from the mesh should be assigned one like any other.
|
||||
|
||||
**3. Nothing in the mesh owns a person's agent configuration.** No catalogue module writes the agent's
|
||||
settings, its tool-server registrations, or the rules and skills the predecessor delivered. On the
|
||||
four machines these are hand-kept, or left over from the predecessor, or missing.
|
||||
|
||||
## What a fix looks like (not decided)
|
||||
|
||||
- **The console provides its endpoint.** A provision, working name `mesh-tools`, served as the URL on
|
||||
the machine port the mesh gives it. The console listens only on loopback, so the provider must be on
|
||||
the consumer's own machine. Co-location already chooses it
|
||||
([ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md)), and a machine with no
|
||||
console refuses the consumer, naming the provision.
|
||||
- **A module for the coding agent requires it** and writes the registration into the agent's
|
||||
system-wide managed settings. The agent reads tool servers from a `managedMcpServers` key there. That
|
||||
file is the machine's rather than a user's, so the module owns it whole and no home directory is
|
||||
named. People keep their own registrations beside it. The agent's separate *exclusive* managed
|
||||
file is the wrong one: it blocks every registration a person makes and hides the hosted connectors.
|
||||
The agent's per-user file is rewritten by the agent continuously and sits in a home directory,
|
||||
which would make its path an operator value. These facts come from the agent's documentation
|
||||
(managed MCP and managed settings pages), not yet verified on a machine.
|
||||
- **The same module owns the rest of the agent's configuration** the predecessor delivered: managed
|
||||
settings and the rules, skills and instructions every session reads. Each declared setting carries
|
||||
a default (ADR 0164,
|
||||
proposed on its own branch), so one configuration serves every machine and one machine may differ.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Is the agent's configuration one module or several?** Tool registration, managed settings, and
|
||||
the instruction files have different readers and change at different rates.
|
||||
- **Whose machine port is the console's?** Should a host-network container that reads its port from
|
||||
`${port:…}` be assigned one, or should a machine-only listener keep its declared number? The second
|
||||
needs a decision, because ADR 0038 does not allow it today.
|
||||
- **Credentials.** The console's authority is the machine's login (ADR 0152). A registration that
|
||||
reaches it carries no secret today. If the console ever listens beyond loopback, the registration
|
||||
needs one, from the vault.
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-02
|
||||
located-in: [mesh-catalog modules/postgres/client.ts (readOnlyQuery)]
|
||||
fixed-by: [mesh-catalog PR 209 (postgres), mesh-catalog PR 210 (mssql)]
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 193 — The store seat's read-only query is read-only by convention, and its answer is unreadable
|
||||
|
||||
## What was observed
|
||||
|
||||
Asking the store seat's `query` verb for a count through the console returned this. Rows are each
|
||||
wrapped in an object under a key named `BEGIN`: the column name, then the value, then the word
|
||||
`ROLLBACK`. A query returning nothing gave the column name and `ROLLBACK` alone. The answer to
|
||||
`select count(*) as n from <table>` was:
|
||||
|
||||
> `rows: [ {BEGIN: "n"}, {BEGIN: "46"}, {BEGIN: "ROLLBACK"} ]`
|
||||
|
||||
A reader can work it out. A program cannot, and a query with two columns loses which value belongs to
|
||||
which.
|
||||
|
||||
## Why this is here
|
||||
|
||||
**The cause is the same line that makes the query read-only.** The holder's tool sends
|
||||
`BEGIN TRANSACTION READ ONLY; <the caller's statement>; ROLLBACK;` to the command-line client as one
|
||||
string. The client prints a command tag for each of the three statements, and the parser takes the
|
||||
first line, `BEGIN`, as the header.
|
||||
|
||||
**And it is not read-only.** [ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
|
||||
decided the store seat's `query` verb is "one read-only statement against one database". The only
|
||||
thing enforcing that is the wrapping transaction, and the caller's statement is pasted inside it as
|
||||
text. A statement that begins by ending the transaction (a commit, then anything) runs whatever
|
||||
follows it outside the read-only transaction, with the holder's own role, the administrative one that creates every
|
||||
consumer's role and database. A rule stated in a decision and enforced by string concatenation is enforced by nothing.
|
||||
|
||||
*This is read from the code, not tried against the live store, and it should not be tried there.*
|
||||
The lab bed is where it gets proven.
|
||||
|
||||
Every caller with `invokes` on the store seat's `query` can do this. The console has `invokes: ["*"]`,
|
||||
so that includes anyone logged in on a machine running the console.
|
||||
|
||||
## What a fix looks like
|
||||
|
||||
- **One statement, refused otherwise.** Send the caller's statement alone, through the client's
|
||||
single-statement path (the extended protocol takes one statement per call and refuses more). The
|
||||
read-only property then comes from the session, not from text around the statement.
|
||||
- **Read-only by role, not by transaction.** Run the verb as a role that can only read, granted
|
||||
`pg_read_all_data`, not as the administrative role. A statement that escapes every wrapper still cannot write.
|
||||
- **Rows as rows.** Parse the client's output with the column names it returns, or use a driver
|
||||
instead of the command-line client, so a row is an object keyed by its columns.
|
||||
- **The check 0159 lacks:** a test that sends a commit followed by a write and asserts the write is
|
||||
refused and nothing changed. Another asserts a two-column row comes back keyed by both columns.
|
||||
|
||||
## Proven, 2026-10-02
|
||||
|
||||
On a throwaway server — the same engine image, no network, reached over a socket — the module's code
|
||||
from the catalogue's main branch ran `COMMIT; COPY (select 1) TO PROGRAM '<a command>'` and **the
|
||||
command ran on the database host** as the server's own user. `COMMIT; DROP TABLE t` executed the drop
|
||||
outside the read-only transaction; the wrapper's own trailing rollback happened to undo it, which a
|
||||
caller ending their statement with a commit of their own would get past (not tried). Nothing was tried
|
||||
against the live store.
|
||||
|
||||
The fix (mesh-catalog PR 209) runs the caller's statement as a login granted `pg_read_all_data` and
|
||||
nothing else, read-only by its role and its session, with a password the mesh mints as one of the
|
||||
module's own secrets; without that password the call is refused rather than run as the admin. On the
|
||||
same throwaway server every escape above, and `SET ROLE`, `RESET SESSION AUTHORIZATION`, turning
|
||||
read-only off, creating a table, altering the role and reading a server file, is refused; a plain
|
||||
select comes back keyed by its columns. One attempt — turning the transaction's read-only off, then
|
||||
deleting — got past the first layer and was stopped by the second, which is why both exist.
|
||||
|
||||
**Not answered by the statement-count fix proposed above.** The command-line client sends one string
|
||||
in one message, so several statements still arrive together. They are harmless as the reader, and
|
||||
refusing them is left to whoever moves the module to a driver.
|
||||
|
||||
## The same hole, elsewhere — and two worse ones
|
||||
|
||||
The `mssql` module wrapped a caller's statement the same way (`BEGIN TRANSACTION; … ROLLBACK;` as its
|
||||
administrator) for its `mssql_query` tool. Its command-line client added two holes of its own. Both
|
||||
were proven on a throwaway server, running the client the way the module ran it:
|
||||
|
||||
- **It substitutes `$(NAME)` from its environment into the caller's text**, and the administrator's
|
||||
password is in that environment. Selecting it as a string returned the password.
|
||||
- **It reads a line beginning `:!!` as a command that starts a program**, in the container that holds
|
||||
the administrator's password and the module's bus credentials. Its switch for refusing such commands
|
||||
makes the shipped version ignore the statement entirely, so the switch cannot be the guard.
|
||||
|
||||
None of it was reachable on the live mesh, for a reason that is a defect of its own: the runtime image
|
||||
never installed the client, so every mssql tool failed (`spawn sqlcmd ENOENT`). The fix (mesh-catalog
|
||||
PR 210) installs the client at a pinned digest and runs the caller's statement as a login that can
|
||||
connect and read and do nothing else. Substitution is off. The statement must be one line, placed after
|
||||
the module's own text, so no line of it can begin a command; a line break is refused before the client
|
||||
starts. On the throwaway server, writes, `xp_cmdshell`, impersonating the administrator, and joining
|
||||
the administrators' role were all refused, and the variable came back as the literal text.
|
||||
|
||||
**The general lesson**, worth more than either module: *a command-line client is an interpreter with
|
||||
its own syntax, and a caller's text handed to it is a program in that syntax as well as in SQL.* A
|
||||
module that passes a caller's text to a client has two languages to defend, and a transaction drawn
|
||||
around the text defends neither.
|
||||
|
||||
## Resolved, 2026-10-02
|
||||
|
||||
Both pull requests merged, built and pushed to the two machines that run each module. Checked live, on
|
||||
every copy, by asking each one who it is:
|
||||
|
||||
- the store seat's `query`, and postgres's own tool on each machine, answer as the reader login —
|
||||
not a superuser, in a read-only transaction — with rows keyed by their columns;
|
||||
- mssql's tool, on each machine, answers as its reader login, outside the administrators' role, and
|
||||
returns `$(SQLCMDPASSWORD)` as the literal text it is. Its tools work for the first time.
|
||||
|
||||
The escapes themselves were tried only on the throwaway servers above; on the live mesh the check is
|
||||
the identity a statement runs as, which is what makes every escape a statement that the login cannot do.
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-10-02
|
||||
located-in: [mesh-catalog modules/dnsmasq, mesh-controller cmd/mesh-controller]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 202 — A module whose required setting nobody set is left out of the machine, and the resolver is the module it happened to
|
||||
|
||||
## What was observed
|
||||
|
||||
Running the controller's own test suite against the catalogue beside it, 2026-10-02.
|
||||
`TestTheResolverIsToldEveryMachineOnTheNetworkAndToldAgainWhenOneLeaves` fails with *"the resolver
|
||||
was not handed the machines"*. Composing the same machine by hand and listing what it receives
|
||||
shows why: **dnsmasq contributes nothing at all.** Four resources are composed for that node, all
|
||||
of them the overlay's. The resolver's package, its configuration, its service and the fact that
|
||||
carries every machine's name are simply not there.
|
||||
|
||||
The cause is one line added to `dnsmasq`'s configuration earlier the same day: the addresses it
|
||||
listens on beside the machine's own became an operator setting,
|
||||
`listen-address=${setting:listen-addresses}`, with no default. A `${setting:…}` nothing sets is
|
||||
refused, a module that cannot be composed is **left out** rather than failing the whole machine
|
||||
([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md)), and so a node
|
||||
assigned the resolver is handed a declaration with no resolver in it.
|
||||
|
||||
The failing test is the symptom that surfaced it. The test is not what is wrong.
|
||||
|
||||
**Proven rather than inferred.** Composing the same machine a second time with
|
||||
`listen-addresses` set to `127.0.0.1` and nothing else changed, every one of dnsmasq's eight
|
||||
resources appears — `needs-broker`, `mesh-state`, `package`, `config`, `runtime-dns`, `runtime`,
|
||||
`service` and `fact-node-zones`. The only difference between a machine with a resolver and a
|
||||
machine without one is whether somebody set a value that did not exist yesterday.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
**Leaving a module out is right, and being quiet about it is not.** The rule exists so one
|
||||
module's broken setting cannot stop a machine converging — a good rule. But the outcome here is a
|
||||
machine that applies cleanly, reports current, and is missing its DNS resolver. Every name on that
|
||||
machine then resolves through whatever was there before, or not at all, and nothing in the mesh
|
||||
says the resolver was dropped. That is the shape
|
||||
[issue 152](../152-a-nodes-plan-failure-silently-drops-its-routed-names/00-report.md) records for
|
||||
routed names, here for a whole module.
|
||||
|
||||
**And a setting with no default is a definition that cannot be assigned.** Every other
|
||||
`${setting:…}` in the catalogue names something that is genuinely particular to one installation —
|
||||
a public domain, an issuer. "Which addresses besides my own do I answer on" has an obvious correct
|
||||
default for every machine that is not a LAN gateway: none beside loopback. A definition that
|
||||
refuses to compose until somebody sets a value most machines do not need is a definition that
|
||||
breaks the next node to be assigned it, and genesis with it.
|
||||
|
||||
## What this does not claim
|
||||
|
||||
Whether the live machines are affected was not checked — those four have had the setting set, or
|
||||
their resolvers would already be gone. The claim is about a machine assigned the resolver *from
|
||||
now on*, and about the silence.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Should the declaration say which modules it left out, where a person or the console can see it?
|
||||
`left_out` already travels to the host ([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md));
|
||||
what is missing is anything that reads it back and says so.
|
||||
- Should a `${setting:…}` be allowed a default in the definition — making "unset" mean "the
|
||||
default" rather than "refuse" — or is a setting with a default no longer the operator's value?
|
||||
- Is leaving a module out ever right for a module a node is **assigned**, as opposed to one it
|
||||
merely pulls in? An assignment is somebody saying *this machine runs this*; silently not running
|
||||
it is the one answer nobody asked for.
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-02
|
||||
located-in:
|
||||
- mesh-controller
|
||||
fixed-by: mesh-controller PR #233
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 203 — A fresh assignment is pushed before its credential exists, and the runtime crash-loops
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-02, the first live assignment of the node tools runtime (to-be 38 WP3). `assign` put the
|
||||
module on a machine and `push` sent the declaration. The host applied everything: the bundle unpacked,
|
||||
the unit written and started, the module's `broker` secret file written and owned by the operator
|
||||
account. The runtime then restarted thirteen times in a minute:
|
||||
|
||||
```
|
||||
mesh-tools: cannot read the broker credential at …/broker: SyntaxError: Unexpected token 'O',
|
||||
"Oj6j2Ssa-v"... is not valid JSON
|
||||
```
|
||||
|
||||
The file held a 40-byte random secret, not a bus credential. The push's own output had said why,
|
||||
one line among forty: *the bus's user list leaves out … `<node>.node-tools`. Each is a user that cannot
|
||||
connect until one is issued.* The credential exists only after `module issue <module> --node <node>`,
|
||||
a separate act; a second push then carried the real credential and the runtime came up. The same
|
||||
sequence repeated on the next two machines, by hand, in the right order.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
Every module that speaks on the bus declares an `own-secrets.broker`; the mesh seals *something*
|
||||
there on assignment and the real credential only on issue. So the first push of any fresh assignment
|
||||
delivers a process that cannot authenticate and will crash-loop until a person runs a second verb and
|
||||
a second push. Nothing refuses the first push, and the warning is a line in a long list that is
|
||||
printed on every push regardless. The design says the mesh issues an assignment's subjects and the
|
||||
runtime serves what it is issued ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md));
|
||||
an assignment whose credential is not issued is half an assignment, and the mesh lets it through.
|
||||
|
||||
## Questions HQ must answer
|
||||
|
||||
- Is issuing the credential part of assigning, so `assign` mints it, or must a push refuse a module
|
||||
whose bus user is unminted, naming the verb?
|
||||
- Is a placeholder sealed where a credential belongs ever right, or should the resource be absent
|
||||
until the credential exists, so the host never writes a file the process cannot read?
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
# Diagnosis — 203
|
||||
|
||||
**2026-10-03.** Two acts, one effect. `assign` records the assignment and, at composition, every
|
||||
`own-secrets` entry a module declares gets a sealed value from the controller's `Needed` map — a
|
||||
value minted so the file exists, which for `broker` is a random secret, not a credential. The bus
|
||||
credential is composed only by `module issue <module> --node <node>` (cmd/mesh-controller/modules.go,
|
||||
`issueOnTheNewBus` → `issueWith`): it mints the bus user, records its hash, and seals the credential
|
||||
JSON into the same `broker` need. Nothing joins the two: `push` composes and sends whatever the need
|
||||
holds, and the only warning is the standing line listing every bus user without a minted credential,
|
||||
printed on every push regardless of what was just assigned.
|
||||
|
||||
Ruled out: the host (it wrote the file it was given, owned as asked); the runtime (it refused a file
|
||||
that is not JSON, correctly, and said so); the manifest (`own-secrets.broker` is the shape every
|
||||
module uses).
|
||||
|
||||
**Owner:** mesh-controller — the assign path. **Fix direction:** assigning a module that declares
|
||||
`own-secrets.broker` issues its credential in the same act, idempotently; a push of a module whose bus
|
||||
user is unminted is refused by name rather than sent with a placeholder.
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-02
|
||||
located-in:
|
||||
- mesh-controller
|
||||
fixed-by: mesh-controller PR #232
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 204 — A controller handover re-sent every node a declaration composed from a stale view
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-02 21:29 UTC. Two machines were assigned the node tools runtime and had the console taken off
|
||||
them, and were pushed; the host on each applied it (the console removed, the runtime created and
|
||||
running). Two seconds later, on each, a second declaration arrived that undid it — the host's log:
|
||||
|
||||
```
|
||||
23:29:18 created node-tools.runtime (node-tools): 239 file(s), running as node-tools.service
|
||||
23:29:18 applied 569 resource(s)
|
||||
23:29:20 removed node-tools.runtime (node-tools)
|
||||
23:29:20 forgotten node-tools.interpreter (nodejs)
|
||||
23:29:24 created mesh-console.needs-broker, created mesh-console.server (mesh-console)
|
||||
```
|
||||
|
||||
The second declaration had the console assigned and the runtime absent: the assignments as they were
|
||||
a minute earlier. The controller's status knew of one send per machine, the person's. At that minute a
|
||||
plan from an unrelated merge was rolling a new controller build onto the control node, so an instance
|
||||
was starting while another was stopping. A third push by hand, two minutes later, restored both
|
||||
machines and nothing undid it again.
|
||||
|
||||
Which instance sent the stale declaration, and from what, is not established: the outgoing one on its
|
||||
way down, the incoming one at start-up before its view was current, or the rolling plan sending what it
|
||||
had composed when it was made — the same family as [issue 201](../201-a-push-recreated-the-controller-behind-the-row-its-successor-wrote/00-report.md),
|
||||
where a plan sent a digest older than the one a successor had written.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
A declaration the mesh sends is the mesh's word on what a machine should be; a machine applies it in
|
||||
full, including removing what it no longer names. A stale one is therefore not a no-op: it tears down
|
||||
whatever was assigned since, and the controller's own record does not show the send, so the next
|
||||
person reads "applied, current" over a machine that is wrong. Two people merging within a minute is
|
||||
ordinary, and a controller handover happens on every controller merge.
|
||||
|
||||
## Questions HQ must answer
|
||||
|
||||
- Where does the stale view come from, and is every send recorded so status can show it?
|
||||
- Should a declaration carry the assignment generation it was composed from, so a host refuses one
|
||||
older than the last it applied, as it already refuses a declaration it cannot verify?
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
# Diagnosis — 204
|
||||
|
||||
**2026-10-03, in the controller's code.**
|
||||
|
||||
Ruled out: a start-up re-send (a starting controller sends nothing; it asserts the bus, resumes plans,
|
||||
follows events); a plan sending a recorded declaration (a plan records artifact digests and module
|
||||
states, and its rollout composes fresh at send time); a cache (every compose reads the store); a path
|
||||
that does not record its send (push, the cascade and the rollout all record after sending; only the raw
|
||||
`declare <node> <file>` command did not); the host applying an older sequence (it already refuses a
|
||||
declaration numbered below the one it kept).
|
||||
|
||||
Found, two faults that together produce the evidence:
|
||||
|
||||
1. **The sequence number went on at send time, after composing.** Every sending path composed first and
|
||||
numbered each declaration as it was sent; a multi-machine send composes every machine before sending
|
||||
any. So a declaration composed *before* an assignment changed and sent *after* a fresher one carried
|
||||
the higher number — and the host, refusing only lower numbers, applied the older content as the mesh's
|
||||
newest word. The stale declaration was accepted, so its number was higher, so it was composed earlier
|
||||
and sent later.
|
||||
2. **The send record was written on the sender's own context, after the send.** A controller being
|
||||
replaced in that second has its context cancelled between telling the machine and writing the record;
|
||||
the machine was told, the record never written, and status showed only the person's earlier send.
|
||||
|
||||
The likely sender, consistent with both and with the timing: the outgoing controller's reaction to a
|
||||
catalogue registration during the build round, which re-sends the machines running the registered
|
||||
module (one ran on exactly the two machines affected), composed under its hold before the person's
|
||||
assignments, numbered and sent at 21:29:19–20 as the controller was being replaced. The old container's
|
||||
log is gone, so the sender is inferred from code and timing; the mechanism is not.
|
||||
|
||||
**Owner:** mesh-controller. **Fix direction:** number a declaration before composing it, in every path,
|
||||
so what was composed earlier is numbered lower whatever order the sends happen in and the host's
|
||||
existing refusal does its job; record a send on a context that outlives the sender; the raw `declare`
|
||||
command records too.
|
||||
|
||||
Two questions left for HQ: whether status should show the sequence a machine was last sent beside the
|
||||
digest, and whether a sender's hold should also cover the assignment verbs, which today run between a
|
||||
hold's compose and its send without waiting.
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-02
|
||||
located-in:
|
||||
- mesh-host
|
||||
- 00-META/how-we-build.md
|
||||
fixed-by: mesh-host PR #79 (the host's half; who keeps a machine current is still a decision to take)
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 205 — A package resource fails against a stale package database, and nothing keeps it fresh
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-02. The node tools runtime's manifest declares the interpreter as a package. On three machines
|
||||
the package installed. On the control node the host failed the declaration three times and reported the
|
||||
machine wrong, stuck:
|
||||
|
||||
```
|
||||
applying "node-tools.interpreter": installing nodejs: pacman exited 1:
|
||||
error: failed retrieving file 'nodejs-26.5.0-1-x86_64.pkg.tar.zst' from <mirror>: 404
|
||||
… (every mirror)
|
||||
error: nodejs: signature from "<packager>" is invalid
|
||||
```
|
||||
|
||||
The machine's package database was from 24 July, ten weeks earlier; the mirrors had long moved on from
|
||||
the version it asked for, and its keyring was as old. The host asks the package manager to install from
|
||||
whatever database the machine has and does not refresh it; refreshing on the host's own initiative is
|
||||
not safe either, because on a rolling distribution a refreshed database plus a single install is a
|
||||
partial upgrade, which the distribution warns against. The way out was a full system upgrade by the
|
||||
operator, outside the mesh.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
A `package` resource is one of the host's shapes and every environment module leans on it (design 37).
|
||||
Its success depends on a machine fact the mesh neither records nor keeps: how old the package database
|
||||
is. A machine that has not been upgraded in months fails every new package the mesh declares, with an
|
||||
error that reads as a mirror outage. The mesh says the operator's machine is the mesh's
|
||||
([ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md));
|
||||
nothing in it says who keeps the machine current enough for its own declarations to apply, or checks.
|
||||
|
||||
## Questions HQ must answer
|
||||
|
||||
- Is keeping a machine's package database and keyring current a module's job (a `package-manager`
|
||||
seat holder with a schedule), the host's, or the operator's — and how is it checked?
|
||||
- Should a `package` resource's failure distinguish "the database is stale" from "the mirror is down",
|
||||
so the report says what to do?
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
# Diagnosis — 205
|
||||
|
||||
**2026-10-03.** The host's package step on an Arch machine installs with the package manager against
|
||||
the database the machine has (mesh-host internal/system/arch.go); it neither refreshes it nor can
|
||||
safely, since a refresh plus one install is the partial upgrade the distribution warns against. On
|
||||
the control node the database and keyring were from 24 July; the mirrors no longer served the version
|
||||
it named, so every mirror answered 404 and the one cached file failed its signature. The host reported
|
||||
the package manager's output whole, which reads as a mirror outage.
|
||||
|
||||
Two owners. The **narrow** half is the host's: classify that failure and say what it is — the database
|
||||
is stale, the operator must upgrade — rather than relaying forty mirror lines. The **wide** half is a
|
||||
rule nobody has written: who keeps a machine current enough for its own declarations to apply, and
|
||||
how that is checked. ADR 0173 makes the machine the mesh's; `00-META/how-we-build.md` says nothing
|
||||
about its package database. That is a decision (playbook 02), not a code fix: a `package-manager` seat
|
||||
holder with a schedule, the host, or the operator by rule.
|
||||
|
||||
Ruled out: the manifest (`package: nodejs` is correct for the distribution and installed on three
|
||||
machines the same hour); the network (the mirrors answered, with 404s).
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-03
|
||||
located-in:
|
||||
- mesh-controller
|
||||
fixed-by: mesh-controller PR #233 (the worker's shape and the plan's order); a credential older than its shape is re-issued by hand, not detected
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 206 — A seat's worker changing type strands its holder, and the build that would fix it
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-03, the switch to shared build work ([ADR 0190](../../02-DECISIONS/0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md)).
|
||||
The controller change made a seat's worker a pull consumer and the build machine pull from it. The
|
||||
merge's plan put the **build machine** in tier 0 and the controller in tier 1, so the new build machine
|
||||
rolled first, onto a bus where the running controller had defined the worker as push:
|
||||
|
||||
```
|
||||
mesh-builder: this machine cannot take work from mesh-build-machine: nats: cannot pull subscribe to
|
||||
push based consumer. The mesh creates that queue and this machine's worker on it, and a build
|
||||
machine may not create one
|
||||
```
|
||||
|
||||
It restarted every few seconds. The old controller kept asking for tier 1 — the new controller image —
|
||||
on that worker, and nothing took it. The only thing that would redefine the worker is the controller
|
||||
that could not be built; there is no verb to run a module's previous build. The way out was a
|
||||
person running the previous build machine image by hand on the control node until the new controller
|
||||
had rolled, then removing it.
|
||||
|
||||
Two smaller faults surfaced on the way and were each a step of the same handover: the build machine's
|
||||
credential, issued on 2026-09-28, carried no `claims`, so the new binary's "serve the seat your
|
||||
credential claims" fell back to the new seat it had no grant for (re-issuing the credential fixed it);
|
||||
and the build machine's container restarts on its environment file, not on its credential, so the
|
||||
re-issued credential reached it only because it was already restarting.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
A consumer's type is part of the contract between the controller that defines a worker and the holder
|
||||
that binds it, and the two are built and rolled by different plans in an order the dependency graph
|
||||
decides, not the contract. Any future change to a worker's shape — ack wait, filters, type — can strand
|
||||
every holder the same way, and when the holder is the build machine, the mesh cannot build its way out.
|
||||
[ADR 0162](../../02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md) orders tiers by
|
||||
artifacts; it says nothing about what must be *running* before what, and
|
||||
[issue 201](../201-a-push-recreated-the-controller-behind-the-row-its-successor-wrote/00-report.md)
|
||||
is the same gap seen from the controller's side.
|
||||
|
||||
## Questions HQ must answer
|
||||
|
||||
- Does the controller own the worker's shape fully — redefining an existing consumer to the shape it
|
||||
derives on every start — or does a holder bind whatever shape it finds? Either answer must hold
|
||||
across a handover where the two are at different versions.
|
||||
- Should a plan that changes the build machine always roll the controller first, or should the mesh
|
||||
keep a way to run a module's previous build without a person on the machine?
|
||||
- A credential issued before claims existed names none: should the mesh re-issue credentials whose
|
||||
shape is older than what the binary reads, or should every holder treat an unclaimed credential as
|
||||
the seat its manifest claims?
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
# Diagnosis — 206
|
||||
|
||||
**2026-10-03.** Three faults in one handover, all the controller's.
|
||||
|
||||
1. **The worker's shape is asserted, not reconciled.** The controller creates a seat's worker if
|
||||
absent (internal/broker, the consumer assertion on start) and leaves an existing one as it is. A
|
||||
change of shape — here push to pull — therefore never reaches a bus that already has the worker
|
||||
until somebody deletes it. The new build machine bound a worker whose type its code no longer
|
||||
speaks.
|
||||
2. **The plan rolled the holder before the definer.** The merge's plan tiered by artifacts (ADR 0162):
|
||||
the build machine's image stands on nothing of the controller's, so it came first. For every other
|
||||
module the order is indifferent; for the holder of the build seat, the controller that defines its
|
||||
worker must run first, or the build that would bring the controller cannot be taken.
|
||||
3. **A credential older than its shape.** The build machine's credential was sealed on 2026-09-28,
|
||||
before credentials carried `claims`; the new binary read none and fell back to the new seat, for
|
||||
which it had no grant. Re-issuing the credential fixed it; nothing had said it was stale.
|
||||
|
||||
Also seen: the build machine's container restarts on its environment file and not on its credential,
|
||||
so a re-issued credential reaches it only by chance (shared with issue 203's fix direction).
|
||||
|
||||
Ruled out: the bus (it refused exactly what the grants and the consumer type said to refuse); the
|
||||
build machine's new code (it did what its credential told it).
|
||||
|
||||
**Fix direction:** the controller reconciles every consumer it owns to the shape it derives, recreating
|
||||
one whose type changed and saying so; a plan rolls the controller before any holder of the build seat;
|
||||
a credential whose shape predates what the binary reads is listed and re-issued.
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-03
|
||||
located-in:
|
||||
- mesh-controller
|
||||
fixed-by: mesh-controller PR #233 (a re-made worker on a history-keeping stream delivers from now on); the two questions on retention and on outcomes for commits already passed stay open for a decision
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 207 — A re-made worker replayed every ask the stream kept, and the mesh re-registered its past
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-03, recovering from [issue 206](../206-a-seats-worker-changing-type-strands-the-holder-and-the-build-that-would-fix-it/00-report.md).
|
||||
The build seat's worker, a push consumer the new controller could not change to pull, was deleted by hand
|
||||
and the controller restarted. On start it recreated the worker in the shape it derives — pull — and with
|
||||
the delivery policy a new consumer gets when nothing says otherwise: *every message the stream holds*.
|
||||
The seat's stream keeps its history. The build machine then took, in order, every build ask since
|
||||
1 October:
|
||||
|
||||
```
|
||||
a build request arrived for …/mesh-catalog.git (build-1790856308080864567)
|
||||
[built] baserow from 6afc1160
|
||||
```
|
||||
|
||||
Each outcome was heard and taken in as any build's is. In the minute before the build machine was taken
|
||||
off the control node to stop it, nine modules were re-registered from the 1 October commit — baserow,
|
||||
cloudflare-dns, gitlab, grafana, icecast, influxdb, jira, keycloak, letta — the catalogue's recorded
|
||||
head moved back to that commit, so status listed almost every module as "behind", and the upgrade
|
||||
policy re-sent the machine running five of them, which replaced their tools containers with the old
|
||||
images. Recovery: the nine rebuilt from main by hand, the worker re-made by hand as pull delivering only
|
||||
new asks, the build machine assigned again.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
A work queue that keeps its history is a replay waiting for a consumer that starts from the beginning,
|
||||
and a new consumer starts there unless told not to. Nothing in the controller's consumer derivation says
|
||||
where a seat's worker starts, so any re-creation — by hand, or by the reconciliation issue 206's fix
|
||||
adds — can replay the mesh's whole build history into the catalogue and onto machines. And the taking-in
|
||||
of an outcome trusts the outcome's commit absolutely: an outcome for a commit older than what the
|
||||
catalogue holds is registered as if it were news, and rolls out.
|
||||
|
||||
## Questions HQ must answer
|
||||
|
||||
- Does a seat's work queue keep acknowledged asks at all? If it is a work queue, acknowledged work
|
||||
should leave it ([design 25](../../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §1 calls it one); if it
|
||||
keeps history for the record, its worker must be derived to start at new messages, always.
|
||||
- Should a build outcome for a commit the catalogue has already moved past be recorded and **not**
|
||||
registered — a build of the past is a fact, not a change — and never sent to machines?
|
||||
@@ -1,13 +0,0 @@
|
||||
# Diagnosis — 207
|
||||
|
||||
**2026-10-03.** The worker was deleted by hand and the controller, on restart, recreated it with the
|
||||
server's default delivery policy — every message the stream holds — on a stream that keeps its history.
|
||||
The build machine then took every ask since 1 October in order, and each outcome was taken in as news:
|
||||
`takeIn` records the build and registers the manifest it carries, whatever commit it is from. Nothing
|
||||
in the consumer derivation said where a seat's worker starts; nothing in the taking-in compared the
|
||||
outcome's commit with what the catalogue already held.
|
||||
|
||||
Owner mesh-controller. Fixed in part: a worker re-made by the controller on a history-keeping stream
|
||||
now delivers from the moment it is made (PR #233). Open for a decision, kept in the report's questions:
|
||||
whether a seat's work queue should keep acknowledged asks at all, and whether an outcome for a commit
|
||||
the catalogue has already moved past should be recorded but never registered or rolled out.
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
status: located
|
||||
opened: 2026-10-03
|
||||
located-in:
|
||||
- mesh-controller
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 208 — A seat's worker is made only when the controller starts, so a holder assigned later finds none
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-03, assigning the first holders of `node-build-agent` ([ADR 0190](../../02-DECISIONS/0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md))
|
||||
to four machines and pushing them. Every agent came up, authenticated, bound its seat, and restarted:
|
||||
|
||||
```
|
||||
taking build work as a holder of node-build-agent
|
||||
mesh-builder: this machine cannot take work from node-build-agent: nats: consumer not found. The mesh
|
||||
creates that queue and this machine's worker on it, and a build machine may not create one
|
||||
```
|
||||
|
||||
The seat's stream existed; its worker did not. The controller makes a seat's worker where it raises
|
||||
the bus's objects — on start — for the seats that have a holder at that moment, by design: *the queue
|
||||
before the holder, so work queues until somebody arrives*. Nothing makes the worker when a holder
|
||||
arrives later: a push asserts the module's own consumers (what it consumes) and not the seat's worker.
|
||||
The remedy was a controller restart, so the raise ran again with the holder known.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
A seat's first holder is assigned after the controller started in every case but genesis, so every
|
||||
new role's first holder meets this. The build machine met it on 2026-09-28 the same way ("left the
|
||||
build machine bound to a consumer nothing had created") and the fix then was to pass the holders at
|
||||
the raise — which fixed the start, not the arrival. The holder says the right thing and cannot do
|
||||
anything about it, because a holder may not create its worker (design 25 §3).
|
||||
|
||||
## Diagnosis
|
||||
|
||||
Owner mesh-controller: the raise runs once (`RaiseSeats` with the holders of the moment); `push` and
|
||||
`assign` run `EnsureConsumer` only for a module's declared consumption. **Fix direction:** when a
|
||||
module claiming a seat with `accepts` is assigned, or on every push that composes a holder for such a
|
||||
seat, ensure the seat's worker as the raise does — the same derivation, the same idempotent assertion.
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-03
|
||||
located-in:
|
||||
- mesh-tools
|
||||
fixed-by: mesh-tools #32
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 209 — A bundle's own copy of the SDK registers into a registry the runtime never reads
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-03, preparing [design 38](../../03-DESIGN/01-to-be/38-building-the-operators-machine.md)
|
||||
WP4 — the first module whose tools bundle the node's runtime would load beside its own. Before the
|
||||
manifest changed, a probe on the laptop did what the runtime does: a process holding one copy of
|
||||
`@novox/mesh-sdk` imported a bundle in another directory that carried its own copy, and the bundle
|
||||
called `registerModuleTools` as every tools entrypoint does.
|
||||
|
||||
```
|
||||
registrations seen by host after importing bundle: 0
|
||||
```
|
||||
|
||||
The import succeeds, the bundle registers, and the runtime's `collectTools` sees nothing. The
|
||||
runtime would log the bundle as loaded and answer `tools` for the module with an empty list: silent,
|
||||
and indistinguishable from a module that serves nothing by choice.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
WP3 found that *a TypeScript bundle must carry its dependencies*, and the toolchain copies them in
|
||||
so a bundle starts anywhere. Among them is the SDK. The runtime imports a bundle in-process, and
|
||||
the language resolves a bare import from the importing file's own tree — so every bundle brings a
|
||||
second SDK into the process: its own registry of tools and its own broker handle. The SDK's registry
|
||||
is a module-level list, and the runtime reads only the one it imported itself.
|
||||
|
||||
Every module from WP4 on is loaded this way. The runtime's tests never met it because their fixture
|
||||
bundles sit under the runtime's own tree and resolve the same copy. Nothing in design 38 WP1 or WP3
|
||||
says which SDK a loaded bundle speaks to, and the one-runtime record
|
||||
([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md))
|
||||
assumes without saying that it is the runtime's.
|
||||
|
||||
## Diagnosis
|
||||
|
||||
Owner mesh-tools (`node-tools`): the runtime imports a bundle's entrypoint and counts the
|
||||
registrations that appear after it, through the SDK it imported; the bundle's `import "@novox/mesh-sdk/tools"`
|
||||
resolves to the copy under the bundle's own `node_modules`. **Fix direction:** the runtime resolves
|
||||
every import of the SDK, from whichever bundle, as if the runtime had written it — one registry,
|
||||
one broker — and leaves everything else a bundle carries to the bundle's own tree. A test loads a
|
||||
bundle from a directory holding its own copy of the SDK and asserts its tools are served. The
|
||||
toolchain keeps copying dependencies in: a bundle launched as a process ([ADR 0188](../../02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md))
|
||||
needs them, and an imported one is simply not allowed to bring a second SDK.
|
||||
|
||||
## Resolution
|
||||
|
||||
2026-10-03, mesh-tools #32: the runtime installs a synchronous resolve hook before the first bundle is
|
||||
imported, sending every import of the SDK, from whichever bundle, to its own copy; a bundle's other
|
||||
dependencies still resolve from its own tree, and a bundle launched as a process is untouched. The test
|
||||
loads a bundle from a directory holding its own copy of the SDK and its own dependency, and sees its
|
||||
tool served with the dependency's answer. Proven live 2026-10-03 by WP4's proof in design 38: the packet filter's bundle, the first loaded beside
|
||||
the runtime's own, serves its four tools on all four machines.
|
||||
@@ -1,64 +0,0 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-03
|
||||
located-in:
|
||||
- mesh-host
|
||||
fixed-by: mesh-host #80
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 210 — The host re-creates the node's runtime on every reconcile, restarting it every ten minutes
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-03, on the laptop, while proving [design 38](../../03-DESIGN/01-to-be/38-building-the-operators-machine.md)
|
||||
WP4. The host's log says the same thing at every reconcile, ten minutes apart, since the runtime
|
||||
module first arrived at 00:04 — 82 times in the day's first thirteen hours:
|
||||
|
||||
```
|
||||
created node-tools.runtime (node-tools): 239 file(s), running as node-tools.service
|
||||
```
|
||||
|
||||
and systemd confirms it: `node-tools.service` is stopped and started at 12:39, 12:49, 12:59, 13:08.
|
||||
Nothing else in those reconciles changed; every other resource is `kept`. The declaration is the
|
||||
same one each time — no push happened between the cycles.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
The runtime is every module's tools on the node ([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
|
||||
A restart every ten minutes drops every call in flight at that moment, re-subscribes every
|
||||
membership, and re-imports every bundle; a bundle that is slow to load leaves the node's tools
|
||||
silent for that long, ten minutes out of every ten. And the log reports it as success, so nothing
|
||||
in `status` shows a node whose tools blink. The host's rule is that a resource reports `unchanged`
|
||||
when the machine is as declared; a `process` here never does.
|
||||
|
||||
## Diagnosis
|
||||
|
||||
Owner mesh-host, `internal/apply/process.go`. The decision is: the digest of the fetched bundle
|
||||
plus the unit text is `want`; when the host's record of what it last wrote equals `want` and no
|
||||
`restart-on` resource changed, the process is left alone. Two things were ruled out on the laptop:
|
||||
the credential the runtime restarts on, which has not been written since the evening before (the
|
||||
host would also say `updated`, not `created`, for a restart it owed to another resource); and the
|
||||
unit text, rendered with sorted keys and so stable. What fails is the record. The host's state file
|
||||
holds an entry for `node-tools.runtime` — applied at the last cycle, with **no `wrote` digest at
|
||||
all** — while the sibling entry for the runtime's credential carries its digest. The apply sets the
|
||||
digest on its outcome on every path that installs the daemon, and the loop that records outcomes
|
||||
copies it into the record for every kind; between the two, a `process` outcome arrives with its
|
||||
digest empty. **Fix direction:** find where a `process` outcome loses its digest on the way to the
|
||||
record, and a test that applies the same `process` declaration twice against a recorded store and
|
||||
asserts the second outcome is `unchanged` with no restart — the test the shape never had. Observed
|
||||
on the laptop's journal and state; the other three machines' host logs are not readable by the
|
||||
operator account over SSH, and the behaviour is the host's, not the machine's.
|
||||
|
||||
## Resolution
|
||||
|
||||
2026-10-03, mesh-host #80. The diagnosis above was right about where and wrong about what: the
|
||||
record is written every cycle, and the process applier's *unchanged* path returned an outcome that
|
||||
said nothing about what was written, so the loop recorded it without the digest. The next cycle,
|
||||
five minutes later, found an empty record and re-created the daemon; the one after was unchanged
|
||||
and erased the digest again. `created` every other cycle is the ten-minute cadence, and it is why the
|
||||
state file held the digest on one read and not on the next. The unchanged outcome now carries the
|
||||
digest forward, as a file's and an archive's do. The test applies one process three times and
|
||||
asserts the record survives an unchanged apply and no restart is asked; it fails on the code before.
|
||||
Proven live on the laptop after the host rolled: two reconcile cycles with no `created
|
||||
node-tools.runtime` line and the runtime's start time unmoved.
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
status: located
|
||||
opened: 2026-10-03
|
||||
located-in:
|
||||
- mesh-controller
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 211 — A bundle is built in the same tier as the toolchain it is compiled in, against the old toolchain
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-03, merging a runtime change that needed a new SDK release. The SDK was published first;
|
||||
then the merge of the tools repository planned two tiers, and the first held both the toolchain
|
||||
images and the runtime's own bundle:
|
||||
|
||||
```
|
||||
> tier 0
|
||||
mesh-tools asked
|
||||
node-tools built from 8e30ea9f
|
||||
```
|
||||
|
||||
The bundle was built while its toolchain image was still being built. A TypeScript bundle's
|
||||
dependencies are copied from the toolchain image it is compiled in
|
||||
([design 38](../../03-DESIGN/01-to-be/38-building-the-operators-machine.md) WP3), so this one was
|
||||
compiled and packed against the toolchain as it stood before the merge — carrying the old SDK —
|
||||
and was recorded as built from the new commit.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
Every bundle compiled by a toolchain depends on the module that publishes that toolchain, and the
|
||||
planner does not know it: a manifest names its toolchain by `language`, not in `build.on`, so the
|
||||
dependency is implicit and the tiers are computed without it. Whenever a change touches the
|
||||
toolchain and a bundle in one merge — or the toolchain's own repository holds a bundle, as this one
|
||||
does — the bundle is built against the previous toolchain and reported current. Nothing fails; the
|
||||
bundle simply carries yesterday's dependencies under today's commit.
|
||||
|
||||
## Diagnosis
|
||||
|
||||
Owner mesh-controller: the planner orders a merge's modules by `build.on`; the builder's toolchain
|
||||
for a bundle (`ToolchainFor(language)`: the module and artifact it is compiled in) is not part of
|
||||
that order. **Fix direction:** the planner treats a bundle's toolchain as a `build.on` it did not
|
||||
have to write — a bundle of language L depends on the module that publishes L's toolchain — so the
|
||||
bundle lands in the tier after it. A test: a merge touching the toolchain module and a TypeScript
|
||||
bundle plans the bundle one tier later. Worked around on the day by building the bundle again once
|
||||
the toolchain was built.
|
||||
Reference in New Issue
Block a user