Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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)).
|
||||
|
||||
@@ -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,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
|
||||
|
||||
-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)
|
||||
@@ -188,6 +188,7 @@ 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)
|
||||
- **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)
|
||||
|
||||
### Its tiers, from the bottom up
|
||||
|
||||
@@ -278,9 +279,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)
|
||||
@@ -288,7 +286,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)
|
||||
|
||||
### How it is built
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +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
|
||||
---
|
||||
|
||||
# 38. Building the operator's machine
|
||||
@@ -98,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.*
|
||||
@@ -125,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
|
||||
@@ -163,16 +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`); the live proof follows node by node.
|
||||
|
||||
## WP4 — The first holder moves: the packet filter
|
||||
|
||||
*mesh-catalog. Half a day. The live proof of ADR 0175.*
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user