Compare commits
148
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
179fd7f83f | ||
|
|
e1203e5a43 | ||
|
|
4ce967619a | ||
|
|
6df2cfecd6 | ||
|
|
73047501f6 | ||
|
|
bf39baf104 | ||
|
|
5eadf36937 | ||
|
|
8c9a2c7501 | ||
|
|
54213ba90c | ||
|
|
7fb59bde98 | ||
|
|
a4d24d7b65 | ||
|
|
116b2d1793 | ||
|
|
68a14493c9 | ||
|
|
98eb3aa76f | ||
|
|
91bbe648a8 | ||
|
|
0e7b85f184 | ||
|
|
dac49de6e7 | ||
|
|
0d9208dbbf | ||
|
|
bf0ee7cb25 | ||
|
|
4567e13071 | ||
|
|
aa5d9f1045 | ||
|
|
79642251a1 | ||
|
|
331cb94c6e | ||
|
|
78351560f7 | ||
|
|
62cc2f89c7 | ||
|
|
426f741ad0 | ||
|
|
9bed54d3be | ||
|
|
413daf8ad5 | ||
|
|
5c993c09b7 | ||
|
|
7c3be48db2 | ||
|
|
b665d06701 | ||
|
|
1bd13446d4 | ||
|
|
14adaafa53 | ||
|
|
bd673cc6ec | ||
|
|
bd6c55d225 | ||
|
|
2bbbc56502 | ||
|
|
c8935aceca | ||
|
|
29b656f2c0 | ||
|
|
d05ac367f1 | ||
|
|
1c0dafb918 | ||
|
|
018ee359ae | ||
|
|
c5535eeebd | ||
|
|
dbb9d2bc16 | ||
|
|
28d53dcc28 | ||
|
|
df667eb710 | ||
|
|
098a2ca485 | ||
|
|
a34cedeb5d | ||
|
|
db5ff5a5ee | ||
|
|
780c2b6e58 | ||
|
|
c4151e6bc4 | ||
|
|
8c231102f8 | ||
|
|
d8083bcf9e | ||
|
|
6f26f97fdb | ||
|
|
63ed4a7c96 | ||
|
|
48ca2fb41b | ||
|
|
3976f09738 | ||
|
|
2904c359b8 | ||
|
|
b277f3b4ba | ||
|
|
50e4d9c2a7 | ||
|
|
a7d0dd83d1 | ||
|
|
a2c9fbb665 | ||
|
|
0278766dfb | ||
|
|
606fbb7add | ||
|
|
a92e4bf121 | ||
|
|
187442ec7b | ||
|
|
47c45d4386 | ||
|
|
82496536cd | ||
|
|
b0de267301 | ||
|
|
b0a74b23fd | ||
|
|
6a5f68d11f | ||
|
|
821cd3b489 | ||
|
|
49b0319230 | ||
|
|
86d1763cfa | ||
|
|
a7e9e6dea0 | ||
|
|
ea0853ca46 | ||
|
|
fcd27c8399 | ||
|
|
03e39316ea | ||
|
|
65a252dc00 | ||
|
|
6be284c781 | ||
|
|
ea9a433385 | ||
|
|
9ddd7215ad | ||
|
|
626af3e8e2 | ||
|
|
180e3b8f7e | ||
|
|
4ae452d0ad | ||
|
|
f35f3757bb | ||
|
|
6b8fb562ce | ||
|
|
2175d13935 | ||
|
|
39f0d64840 | ||
|
|
eef03f2b08 | ||
|
|
64acc94de8 | ||
|
|
99eb322de5 | ||
|
|
5460681117 | ||
|
|
0acb47fa55 | ||
|
|
7a633f2780 | ||
|
|
bef510fda2 | ||
|
|
c58f4d6790 | ||
|
|
d29d3dfc23 | ||
|
|
d85b41ae4d | ||
|
|
e00e3bc3ce | ||
|
|
b8d8101c45 | ||
|
|
85961f8348 | ||
|
|
48a620249b | ||
|
|
7f0fe27cf2 | ||
|
|
bfc410fafe | ||
|
|
d436122be2 | ||
|
|
76a535e69e | ||
|
|
027e5b8d73 | ||
|
|
79d1619f16 | ||
|
|
5a6b7ca4f8 | ||
|
|
e1f2c6bd5b | ||
|
|
cf8134e318 | ||
|
|
35f7f4401b | ||
|
|
62d61938ad | ||
|
|
f841845b0d | ||
|
|
3627f7e9db | ||
|
|
2ffe1d0915 | ||
|
|
763e327610 | ||
|
|
93f828c5eb | ||
|
|
fe706af63a | ||
|
|
52e9df0f02 | ||
|
|
36454d7e4a | ||
|
|
e84c822e89 | ||
|
|
a170913202 | ||
|
|
9a20c16d9b | ||
|
|
8bd0ca0bdc | ||
|
|
598f6a8952 | ||
|
|
3341c037cb | ||
|
|
22a28ad548 | ||
|
|
1e1957a9c4 | ||
|
|
5292f4176a | ||
|
|
822e8b03f8 | ||
|
|
a82941ee0c | ||
|
|
9ffb7eec55 | ||
|
|
7499f1e50c | ||
|
|
214b486a50 | ||
|
|
90b44a48df | ||
|
|
860331dc37 | ||
|
|
d0044cf555 | ||
|
|
105ae9a56a | ||
|
|
ee801a6441 | ||
|
|
90b89aa1c9 | ||
|
|
e33191161d | ||
|
|
a0a930b1cd | ||
|
|
8d9c9ab6b5 | ||
|
|
49b1136ded | ||
|
|
743051efe7 | ||
|
|
e8470057aa | ||
|
|
39340fcd76 |
@@ -131,6 +131,22 @@ def main():
|
|||||||
else:
|
else:
|
||||||
seen[number] = name
|
seen[number] = name
|
||||||
|
|
||||||
|
# And decision records, which 155's fix left out: on 2026-10-02 two ADRs numbered 0169 landed
|
||||||
|
# on main from two sessions within the hour, and every check passed.
|
||||||
|
seen_records = {}
|
||||||
|
for path in sorted(glob.glob(os.path.join(ROOT, "02-DECISIONS", "[0-9]*.md"))):
|
||||||
|
name = os.path.basename(path)
|
||||||
|
number = name.split("-", 1)[0]
|
||||||
|
if not number.isdigit():
|
||||||
|
continue
|
||||||
|
if number in seen_records:
|
||||||
|
bad(os.path.join("02-DECISIONS", name),
|
||||||
|
"is numbered %s, and so is %s -- a record's number is how it is cited. Take the next "
|
||||||
|
"free number across main AND every open pull request; the branch that lands last "
|
||||||
|
"renumbers" % (number, seen_records[number]))
|
||||||
|
else:
|
||||||
|
seen_records[number] = name
|
||||||
|
|
||||||
for path in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))):
|
for path in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))):
|
||||||
front = frontmatter(path)
|
front = frontmatter(path)
|
||||||
if front is None:
|
if front is None:
|
||||||
|
|||||||
+23
-2
@@ -52,8 +52,11 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
|
|||||||
|
|
||||||
- **package** — what code resolves when it is **compiled**: an npm/cargo/pypi dependency, by
|
- **package** — what code resolves when it is **compiled**: an npm/cargo/pypi dependency, by
|
||||||
**version**. Served by the **package-registry** (gitea). Only a builder talks to it.
|
**version**. Served by the **package-registry** (gitea). Only a builder talks to it.
|
||||||
- **artifact** — what the mesh delivers to a machine to **install and run**: an OCI image, by
|
- **artifact** — anything a build produces and the mesh delivers to a machine by **digest**: an
|
||||||
**digest**. Served by the **artifact-store** (distribution). Every node pulls from it.
|
`image`, a mirrored `upstream` image, a `bundle` of the module's own code, an `archive`. Served by
|
||||||
|
the **artifact-store**, an OCI registry that holds every kind as content-addressed blobs
|
||||||
|
([ADR 0156](../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)).
|
||||||
|
Every node pulls from it. An image is one kind of artifact, and a module is not an image.
|
||||||
- These are two protocols, not one store being weak — see [ADR 0075](../02-DECISIONS/0075-two-stores-and-which-provides-what.md).
|
- These are two protocols, not one store being weak — see [ADR 0075](../02-DECISIONS/0075-two-stores-and-which-provides-what.md).
|
||||||
|
|
||||||
## How modules relate to the mesh
|
## How modules relate to the mesh
|
||||||
@@ -92,3 +95,21 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
|
|||||||
A new name for an existing thing lands here first, in the same change that introduces it in code. A
|
A new name for an existing thing lands here first, in the same change that introduces it in code. A
|
||||||
record under `02-DECISIONS/` keeps whatever word it was written with — those are immutable — so a
|
record under `02-DECISIONS/` keeps whatever word it was written with — those are immutable — so a
|
||||||
term retired here may still appear there, and the mapping above is how to read it.
|
term retired here may still appear there, and the mapping above is how to read it.
|
||||||
|
|
||||||
|
## The operator's machine
|
||||||
|
|
||||||
|
- **node tools** — the one tool runtime per node, a host-side process the host supervises, that loads
|
||||||
|
every assigned module's tools bundle and serves every tool and held seat's verb on the subjects the
|
||||||
|
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 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)).
|
||||||
|
One of the two ways a node varies a module; the other is a **setting**.
|
||||||
|
- **installed / holding** — a module may be assigned (its package installed, its files placed) without
|
||||||
|
holding the seat its family declares; *holding* is being the one — the login shell, the display
|
||||||
|
session — on that node ([ADR 0176](../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)).
|
||||||
|
- ~~flavor~~ — not used. What a flavor varied is a setting or a separate module.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
status: graduated
|
||||||
|
initiated: 2026-10-02
|
||||||
|
touches:
|
||||||
|
- 02-DECISIONS/0040-what-a-module-is.md
|
||||||
|
- 02-DECISIONS/0011-managed-files-are-generated-never-edited.md
|
||||||
|
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||||
|
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
||||||
|
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
||||||
|
- 02-DECISIONS/0161-what-deserves-a-seat.md
|
||||||
|
- 03-DESIGN/01-to-be/05-the-node-host.md
|
||||||
|
- 03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md
|
||||||
|
- 03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md
|
||||||
|
- 03-DESIGN/01-to-be/34-the-console.md
|
||||||
|
- 03-DESIGN/00-as-is/10-module-catalogue.md
|
||||||
|
- 04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md
|
||||||
|
- 04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md
|
||||||
|
became:
|
||||||
|
- 03-DESIGN/01-to-be/37-the-operators-machine.md
|
||||||
|
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
|
||||||
|
- 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md
|
||||||
|
- 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
|
||||||
|
- 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md
|
||||||
|
- 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 018 — The operator's machine as modules
|
||||||
|
|
||||||
|
**What.** The mesh owns the whole machine, not only the services on it. Everything a person
|
||||||
|
configures on a node — the login manager, the display server, the window manager, the shell, the
|
||||||
|
terminal, the launcher, the notifier, the audio setup, the boot images, the downloads folder, the
|
||||||
|
agent at the terminal — is a module: a package, the files it owns under `/etc` and under the
|
||||||
|
operator's home, the seat it holds, the tools it serves. One default configuration per module,
|
||||||
|
varied per node only through settings rendered into the file or a kept operator region, never
|
||||||
|
through an edit. The servers take the universal modules (shell, prompt, git, the agent); the
|
||||||
|
workstations take those and the graphical stack, which a capability the machine reports gates.
|
||||||
|
This effort writes that behaviour down, measures what the predecessor's desktop modules actually
|
||||||
|
contain, and settles what the mesh must gain before the first of them can be written.
|
||||||
|
|
||||||
|
**Why.** The predecessor is retired on every node. What it still owned on the two workstations —
|
||||||
|
about thirty modules' worth of dotfiles, user units and `/etc` files — is now owned by nothing:
|
||||||
|
no generator regenerates them, and a fix to one of them is a hand edit that nothing records. The
|
||||||
|
migration scoped these modules out as *the workstation's own environment*, and
|
||||||
|
[to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) names them as the last
|
||||||
|
thing the predecessor was keeping alive. To-be 29 covers one directory, `~/.ssh`, and draws a
|
||||||
|
boundary inside it. The operator wants no boundary: the machine is the mesh's, as far as it makes
|
||||||
|
sense to configure it. That is a wider scope than any design states, and it reaches three records
|
||||||
|
that were written for services: what a module is, where a module's tools run, and what a managed
|
||||||
|
file may be.
|
||||||
|
|
||||||
|
**What it touches.** The module definition ([ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md)),
|
||||||
|
seats and their contracts ([ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md)),
|
||||||
|
where a module's tools run ([ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md),
|
||||||
|
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
|
||||||
|
[to-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §6), the host's vocabulary
|
||||||
|
([to-be 05](../../03-DESIGN/01-to-be/05-the-node-host.md)), managed files and settings
|
||||||
|
([ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md),
|
||||||
|
[issue 168](../../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)),
|
||||||
|
and the catalogue's shape ([as-is 10](../../03-DESIGN/00-as-is/10-module-catalogue.md)).
|
||||||
|
|
||||||
|
**Documents.**
|
||||||
|
|
||||||
|
- [01 — The intended behaviour](01-the-intended-behaviour.md): the operator's wish, written as
|
||||||
|
how the mesh behaves, in the mesh's own words.
|
||||||
|
- [02 — What exists, and what is missing](02-what-exists-and-what-is-missing.md): the
|
||||||
|
predecessor's desktop modules measured; which records already say what is wanted; the gaps.
|
||||||
|
- [03 — One tool executor per node](03-one-tool-executor-per-node.md): where a module's tools
|
||||||
|
run. The direction the operator set, the evidence for it, and what it supersedes.
|
||||||
|
- [04 — The seats of the environment](04-the-seats-of-the-environment.md): the roles a machine
|
||||||
|
has once, their candidate contracts, and what gates each.
|
||||||
|
|
||||||
|
**What it had to settle, and where each landed.** *(Graduated 2026-10-02.)*
|
||||||
|
|
||||||
|
1. A module is one *managed thing*, software or not, and every module may serve tools — or ADR
|
||||||
|
0040 already says this and only its examples are narrow.
|
||||||
|
2. One tool executor per node, host-side, module-agnostic; which records it supersedes and
|
||||||
|
in what form the console continues.
|
||||||
|
3. Per-node variation is a setting rendered into the file or a kept region, never an edit —
|
||||||
|
ADR 0011 stands — and issue 168 is fixed before any environment module carries a setting.
|
||||||
|
4. User-scoped units on the host's `service` shape, and a service-manager seat whose holder
|
||||||
|
serves the tools about them.
|
||||||
|
5. The operator account stated on every node; today no node record carries one.
|
||||||
|
6. The seats of the environment and their verbs, one record per seat, slowly, because a
|
||||||
|
seat's tools bind every future holder.
|
||||||
|
7. Where the environment modules live: this catalogue, or one of their own as the media chain
|
||||||
|
has; and whether a third-party organisation's tooling belongs in a public catalogue at all.
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# 01 — The intended behaviour
|
||||||
|
|
||||||
|
*Written 2026-10-02 from the operator's words, in the mesh's words. What is wanted, before what
|
||||||
|
exists. Where a sentence restates a record, the record is named; where it goes further, that is
|
||||||
|
said.*
|
||||||
|
|
||||||
|
## The machine is the mesh's
|
||||||
|
|
||||||
|
**Everything configurable on a node is declared by a module.** Not only the services the mesh
|
||||||
|
runs: the login manager, the display server, the window manager, the bar, the launcher, the
|
||||||
|
notifier, the compositor, the lock screen, the terminal emulator, the clipboard, the shell and its
|
||||||
|
prompt, the editor, the audio setup, the boot images, the package manager's configuration, the
|
||||||
|
agent a person runs at a terminal, and the folders a person works in — a downloads folder that is
|
||||||
|
tidied, backed up, distributed to other nodes and asked questions of. System folders and the
|
||||||
|
operator's home alike. The operator is the only person on every node, so the mesh manages the
|
||||||
|
person's machine, not a machine with a person on it.
|
||||||
|
|
||||||
|
This is [ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md)'s definition applied without
|
||||||
|
the service bias its examples carry. A module is one managed thing, named once, described
|
||||||
|
completely by its manifest. It may have a package, files, a container, a unit, a binary, a seat it
|
||||||
|
holds, and tools it serves — any one of these, or all, or two. There is **no kind of module**: zsh
|
||||||
|
has a package, files, a seat claim and the tools that claim obliges it to serve; downloads has a
|
||||||
|
folder, a process and tools; nftables has a package, files, a service, a seat and tools. The
|
||||||
|
difference is what each declares, not what each is.
|
||||||
|
|
||||||
|
**The home has no boundary.** [To-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md)
|
||||||
|
owns one directory under the home and draws a line inside it between the mesh's and the person's.
|
||||||
|
Here the line is drawn only by what the modules declare: every file some module places is the
|
||||||
|
mesh's; what no module declares is found and left alone, exactly as the adoption rules already
|
||||||
|
say for a machine. The reach is bounded by sense, not by a rule — the mesh configures what can
|
||||||
|
be configured, and a person's documents, projects and history are data under
|
||||||
|
[ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md), not configuration.
|
||||||
|
|
||||||
|
**A module names no node and no path.** The operator account is a node fact and the home is
|
||||||
|
derived from it ([to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §1–2,
|
||||||
|
shipped in the controller; its record is proposed in an open change). A module places a file
|
||||||
|
*under the home, owned by the account*, and the same manifest lands on a server and a laptop.
|
||||||
|
|
||||||
|
## One default, varied by settings, never by edits
|
||||||
|
|
||||||
|
**One module, one default configuration.** The window manager module ships the configuration
|
||||||
|
that is right for every node. There are no flavors: the predecessor's one desktop module carried
|
||||||
|
four, one per class of machine, and what differed between them is what settings are for.
|
||||||
|
|
||||||
|
**A node varies a module in exactly two ways.** A **setting**, declared by the module with its
|
||||||
|
type, meaning and default (proposed alongside the container-runtime records), set for the mesh
|
||||||
|
or for one node, and rendered into the file at composition — the value is in the file, not in an
|
||||||
|
environment variable the file reads. Or a **kept region**: a block in a file the mesh writes
|
||||||
|
*into*, where the operator's own lines survive every push
|
||||||
|
([ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md)). An
|
||||||
|
edit to a managed file outside such a region is not a third way; it is overwritten, as
|
||||||
|
[ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md) says, and the
|
||||||
|
predecessor's habit of adopting disk drift back into its database is not carried over.
|
||||||
|
|
||||||
|
The predecessor's theming — some ninety environment variables substituted into templates at sync
|
||||||
|
time, with tools to list and set them — is the same idea with the wrong rendering. The knobs
|
||||||
|
become declared settings; the file carries the value.
|
||||||
|
|
||||||
|
## Roles a machine has once are seats, and seats carry tools
|
||||||
|
|
||||||
|
**A role a machine fills at most once is a node-scoped seat**, declared by a module
|
||||||
|
([ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md),
|
||||||
|
[ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)): the login shell, the
|
||||||
|
display session, the display server, the terminal emulator, the launcher, the notifier, the
|
||||||
|
compositor, the lock screen, the service manager, the boot loader. Several modules may be able to
|
||||||
|
hold one — zsh, fish and bash can all hold the login shell — and the assignment on each node says
|
||||||
|
which does. Installing a shell is installing software; holding the seat is being *the* shell.
|
||||||
|
|
||||||
|
**A seat's contract is its tools** ([ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
|
||||||
|
Every holder of the login-shell seat serves `execute`, which takes one string, the command, and
|
||||||
|
runs it on the node the seat is scoped to. Every holder of the boot seat serves "rebuild the boot
|
||||||
|
images", so *"rebuild your boot images"* is a verb addressed to a machine, not a one-off step in
|
||||||
|
a hook. Every holder of the service-manager seat answers for the units on the machine, system and
|
||||||
|
user scope. A module may serve its own tools beside the seat's
|
||||||
|
([ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) §2): show the rendered
|
||||||
|
configuration, set a theme value, report status.
|
||||||
|
|
||||||
|
**Any tool may be called from any node.** The operator's statement, and the grant model it
|
||||||
|
implies: the executor on each node may call everything, as the console already may. A verb that
|
||||||
|
needs root on the machine is the module's concern — the tool escalates, the executor and the
|
||||||
|
caller do not know.
|
||||||
|
|
||||||
|
## Servers and workstations differ by capability, not by catalogue
|
||||||
|
|
||||||
|
The same catalogue serves every node. A module declares what it needs — a graphical session, a
|
||||||
|
display server, a container runtime — and the machine reports what it has, as the profile already
|
||||||
|
reports eight capabilities today ([issue 160](../../04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md)).
|
||||||
|
Assignment refuses the wrong placement by name
|
||||||
|
([ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md) §3). So every node takes the shell,
|
||||||
|
the prompt, git and the agent; only a node with a graphical session can take the display server,
|
||||||
|
and only a node holding the display server can take a window manager. Nothing in a module says
|
||||||
|
"workstation".
|
||||||
|
|
||||||
|
## What the operator would say to the mesh
|
||||||
|
|
||||||
|
*Set the login shell on the build node to fish. Rebuild the laptop's boot images. Show me the
|
||||||
|
window manager's effective configuration on the desktop and where each value comes from. Give
|
||||||
|
the downloads folder on the laptop to the home server. Run `uptime` on every node.* Each of these
|
||||||
|
is a seat verb or a module tool, addressed to a node, answered by whatever holds the role there.
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
# 02 — What exists, and what is missing
|
||||||
|
|
||||||
|
*Measured 2026-10-02 on one installation: two workstations, two servers, all four converged to
|
||||||
|
the mesh; the predecessor retired on the last workstation the day before. Numbers are from the
|
||||||
|
machines and the repositories, not from memory.*
|
||||||
|
|
||||||
|
## 1. What the predecessor's desktop looks like
|
||||||
|
|
||||||
|
The predecessor's catalogue on the laptop held **34 modules**, of which **28** are the operator's
|
||||||
|
environment rather than services. By what they declare:
|
||||||
|
|
||||||
|
| shape | count | examples |
|
||||||
|
|---|---|---|
|
||||||
|
| package only | 9 | browser, mail client, process monitor, media player, file manager, chat |
|
||||||
|
| package + `/etc` files + system service | 5 | login manager, display server, power and thermal daemons, package manager configuration |
|
||||||
|
| package + files under the home | 6 | shell and prompt, the agent at the terminal, scripts, the sync client, a music player |
|
||||||
|
| files under the home + user units + hooks | 2 | the desktop environment, audio |
|
||||||
|
| third-party organisation tooling | 6 | out of scope here |
|
||||||
|
|
||||||
|
**The desktop module alone** declares **88 files**, **4 flavors** (the window-manager stack, and
|
||||||
|
one per class of machine), **2 user units** with a hook to enable them, 8 files under `/etc`, a
|
||||||
|
wallpaper shipped as an asset, and reads **about 90 environment variables** as theme knobs,
|
||||||
|
substituted into its templates at sync time and set through a theming tool. Its hook exists
|
||||||
|
because *shipping a unit file does not run it*: one unit had been deployed for months and ran on
|
||||||
|
one machine only, because somebody had enabled it there by hand.
|
||||||
|
|
||||||
|
**The shell module** ships `~/.zshrc`, the prompt configuration, an `~/.ssh/config` that the
|
||||||
|
predecessor generated from its registry, and a `LOGIN_SHELL` variable applied with `chsh` by a
|
||||||
|
hook. Two flavors: the prompt theme, and autocompletion.
|
||||||
|
|
||||||
|
**Other modules write into the desktop module's files.** The chat client places i3 and notifier
|
||||||
|
snippets into `config.d` directories the desktop module owns, and its launch flags, window
|
||||||
|
placement and notification colours are each a variable with a default.
|
||||||
|
|
||||||
|
**One-off steps live in hooks** across the set: enable user units, `chsh`, create a swap file,
|
||||||
|
`mkinitcpio`, enable a vendor VPN service the package ships disabled. Every one is state the
|
||||||
|
host could declare or a verb a seat could serve; none is today.
|
||||||
|
|
||||||
|
## 2. What the migration did with them
|
||||||
|
|
||||||
|
The migration's module to-do scoped the whole set out as *desktop / workstation ricing — the
|
||||||
|
workstation's own environment* and *node/OS tooling — managed on the node, never catalogue*. The
|
||||||
|
last workstation's runbook then split the same set three ways: **A**, system scope, which the
|
||||||
|
host's vocabulary can express today (the login manager, the display server, the power daemons,
|
||||||
|
the package manager, the container runtime); **B**, under a home or a user unit, waiting on
|
||||||
|
to-be 29; **C**, package only, the operator's call. The migration log closes the workstation with
|
||||||
|
*the operator's desktop awaiting its design*.
|
||||||
|
|
||||||
|
Two things followed from scoping them out. Nothing regenerates those files now, so a fix is a hand
|
||||||
|
edit — the login manager's session script was fixed this way on the day of writing, and recorded
|
||||||
|
in a repository nothing deploys from. And the one piece of this family written as a mesh module,
|
||||||
|
the ssh client, was closed on hold in the catalogue until the controller carried the account fact.
|
||||||
|
|
||||||
|
## 3. What the records already give
|
||||||
|
|
||||||
|
| wanted | record | state |
|
||||||
|
|---|---|---|
|
||||||
|
| one module per managed thing; every module may have tools | [ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md) | accepted; examples are services, and the shell is named as a *shared* seat |
|
||||||
|
| a module declares its own node-scoped seat | [ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) | accepted |
|
||||||
|
| a seat's contract is its tools; a holder may add its own | [ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) | accepted; one node seat serves verbs live |
|
||||||
|
| a capability the machine reports gates a holder | [ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md) §3 | accepted; the profile already reports `graphical-session` |
|
||||||
|
| the account as a node fact; a file under the home owned by it | [to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §1–2 | built in the controller; its record proposed in an open change |
|
||||||
|
| inside a home: owned, written into, written by the module, found | proposed in the same change | proposed |
|
||||||
|
| a setting declared with type, meaning, default and cost | proposed with the container-runtime records | proposed |
|
||||||
|
| a managed file is derived; an edit is overwritten | [ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md) | accepted |
|
||||||
|
| the mesh writes into a shared file, never over it | [ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md) | accepted |
|
||||||
|
| a module names no path; the host resolves the home | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) | accepted |
|
||||||
|
| the `user` shape: a login shell is declared state | [to-be 05](../../03-DESIGN/01-to-be/05-the-node-host.md) | designed; used by no module |
|
||||||
|
|
||||||
|
## 4. What is missing
|
||||||
|
|
||||||
|
1. **The account is recorded nowhere.** The node record has the column; on all four nodes it
|
||||||
|
is empty. Every home-scoped module is unassignable until the operator states it.
|
||||||
|
2. **User-scoped units.** The host's `service` shape has no user scope. To-be 29 says it
|
||||||
|
plainly: *a workstation's per-user daemons have no form the mesh can send.* The desktop
|
||||||
|
module's two units, the audio masks, the power module's memory guard and the thermal
|
||||||
|
daemon's profile switcher all need it.
|
||||||
|
3. **One-off steps.** `mkinitcpio`, `chsh`, creating a swap file. Each is either declared
|
||||||
|
state the host lacks a shape for, or a verb a seat should serve. An action in a declaration
|
||||||
|
is refused over the link, and rightly.
|
||||||
|
4. **Settings leak** ([issue 168](../../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)):
|
||||||
|
a setting reaches every mergeable file and every contribution of its module. Ninety theme
|
||||||
|
knobs on that mechanism would reach ninety files. The proposed settings record says a setting
|
||||||
|
names the file it lands in; that has to ship first.
|
||||||
|
5. **Where tools run.** Every module that serves a tool today does so from its own container
|
||||||
|
per node. See [03](03-one-tool-executor-per-node.md).
|
||||||
|
6. **A seat's verbs are undecided for every seat but three.** To-be 33 leaves which verbs each
|
||||||
|
seat serves as *a decision per seat, slowly*. The environment adds a dozen seats.
|
||||||
|
7. **Catalogue placement.** The media chain left this catalogue for its own; whether the
|
||||||
|
environment does the same, and whether a third-party organisation's tooling belongs in a
|
||||||
|
public catalogue, are unasked.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# 03 — One tool executor per node
|
||||||
|
|
||||||
|
*The direction the operator set on 2026-10-02, the evidence it rests on, and what it supersedes.
|
||||||
|
A direction, not yet a decision: the record is written when this effort graduates.*
|
||||||
|
|
||||||
|
## Where tools are served today
|
||||||
|
|
||||||
|
[To-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) names three families — a
|
||||||
|
role's tools on the seat, a module's own tools on the module, the mesh's own verbs on the
|
||||||
|
controller seat — and [ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||||
|
says a runtime serves the subjects its membership issues. What *runs* that runtime is
|
||||||
|
[ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md):
|
||||||
|
one supervised process per module, under the module's own account, carrying that module's
|
||||||
|
compiled tools. Measured on the live mesh:
|
||||||
|
|
||||||
|
| who answers | how it runs | count |
|
||||||
|
|---|---|---|
|
||||||
|
| the mesh's own verbs | the controller binary, on its node | 17 verbs |
|
||||||
|
| the store seat | the store's own runtime | 2 verbs |
|
||||||
|
| the packet-filter seat | **a container per node**, built on the tool-runtime base image, with the network namespace and `NET_ADMIN`, on all four nodes | 3 verbs and 1 own tool |
|
||||||
|
| every module's own tools | the module's container, one per node it runs on | 67 tools across the catalogue |
|
||||||
|
| the console | a container per node, loopback MCP, `invokes: *` | serves none, calls all |
|
||||||
|
| the host | — | serves nothing; answers no question about the machine |
|
||||||
|
|
||||||
|
**The packet-filter holder is the case to look at.** The module is a package, three files and a
|
||||||
|
system service. To serve three verbs it also declares a built image and a container on every
|
||||||
|
node whose only job is to answer them. Scaled to the environment — a shell, a prompt, a launcher,
|
||||||
|
a notifier, a compositor, a login manager, a service manager, a boot loader, a downloads folder —
|
||||||
|
that is one container per module per node for software that is itself not a container, and the
|
||||||
|
operator's judgement is that tools should not run inside a container at all.
|
||||||
|
|
||||||
|
## The direction
|
||||||
|
|
||||||
|
**One tool executor per node, on the host side.** A process the host supervises, the way the
|
||||||
|
launcher supervises the host ([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)): not a
|
||||||
|
container, one bus credential for the node, module-agnostic. It loads the tool code of every
|
||||||
|
module assigned to the node and serves each module's tools and each held seat's verbs on the
|
||||||
|
subjects the membership issues — nothing changes in what [ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
|
||||||
|
and [ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||||
|
say about subjects, grants and memberships; what changes is that one process subscribes for the
|
||||||
|
node instead of one per module.
|
||||||
|
|
||||||
|
- **A module brings its tools as a built artifact**, a bundle the pipeline produces, never an
|
||||||
|
image. The executor knows bundles and subjects; it knows nothing of zsh or nftables.
|
||||||
|
- **A tool is code the module wrote**, one function behind an MCP verb. `execute` on the shell
|
||||||
|
seat is a function with a string argument. The executor does not declare, template or
|
||||||
|
interpret tools; it runs them.
|
||||||
|
- **Root is the module's concern.** A tool that must change the packet filter or rebuild boot
|
||||||
|
images escalates itself. The executor does not run as root for everyone, and the caller does
|
||||||
|
not know.
|
||||||
|
- **Any node may call any tool on any node.** The executor's credential may call everything,
|
||||||
|
as the console's already does; per-module grants on the calling side are not kept.
|
||||||
|
- **The mesh's own verbs stay with the controller** ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)),
|
||||||
|
and a mesh-scoped seat's verbs run on the node that holds it
|
||||||
|
([ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)).
|
||||||
|
No hub is added; the controller's node is already one.
|
||||||
|
|
||||||
|
**The console is the executor, renamed.** It already runs on every node with a credential that
|
||||||
|
may call everything, and it already serves the mesh's tools to whoever is on the machine over
|
||||||
|
MCP on loopback ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
|
||||||
|
It moves out of its container into the host's process tree, gains the serving half, and takes a
|
||||||
|
name that says what it is — *the node's tool runtime* or simply *node tools*; "console" names
|
||||||
|
the operator's half only.
|
||||||
|
|
||||||
|
## What it supersedes, and what it keeps
|
||||||
|
|
||||||
|
| record | effect |
|
||||||
|
|---|---|
|
||||||
|
| [ADR 0047](../../02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md), [ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md) | superseded *for tools*: one process per node runs every module's tool code, under one account. A module's long-running service — a daemon, a container — is untouched; the executor runs tools, not services. The record must say why one account for every module's tools is acceptable: every tool may be called from every node anyway, and root is taken by the tool, not granted to the process |
|
||||||
|
| [ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md) | kept in substance — a module, assigned per node, loopback MCP, the machine's login is the authority — changed in form: host-side, not a container; serves as well as calls; renamed |
|
||||||
|
| [to-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §6, [to-be 34](../../03-DESIGN/01-to-be/34-the-console.md) | amended the same way |
|
||||||
|
| [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) §3, *a container may ask for a capability* | moot for that holder: the verbs run on the host side and escalate as they need |
|
||||||
|
| the container-runtime seat, proposed in an open change: *the holder runs as a supervised process and serves the verbs locally to the host and on the bus* | consistent — a supervised process serving verbs is what the executor is; the open question is whether that holder keeps its own process or serves through the executor like everyone else |
|
||||||
|
| the tool-runtime base image | no longer the way tools reach a node; may remain the way a module's *service* is built |
|
||||||
|
|
||||||
|
## What stays open
|
||||||
|
|
||||||
|
- **The executor's language.** The host is a static Go binary and loads no plugins, so the
|
||||||
|
executor is a sibling process, and its language decides the language of every tool bundle.
|
||||||
|
One decision, taken once.
|
||||||
|
- **How a bundle reaches the node.** An artifact of the module's build, delivered as the host
|
||||||
|
delivers everything else; whether it is a file resource in the declaration or a thing the
|
||||||
|
executor fetches by digest.
|
||||||
|
- **Reload.** A push that adds or upgrades a module's bundle reaches a running executor as a
|
||||||
|
reload, not a restart, or every tool on the node blinks on every push.
|
||||||
|
- **The host's own questions.** [Issue 160](../../04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md)
|
||||||
|
wants a machine to say more about itself. With an executor on every node, "what is this
|
||||||
|
machine made of" is a seat verb like any other, served there.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# 04 — The seats of the environment
|
||||||
|
|
||||||
|
*Candidates, not decisions. To-be 33 says which verbs a seat serves is a decision per seat,
|
||||||
|
taken slowly, because a seat's tools bind every future holder. This document lists the roles the
|
||||||
|
operator's machine has once, who could hold each, what gates it, and a first verb or two — so
|
||||||
|
each record has a starting point.*
|
||||||
|
|
||||||
|
## The rule for what is a seat here
|
||||||
|
|
||||||
|
A role the machine fills **at most once** is a node-scoped seat, declared by the module family
|
||||||
|
that fills it ([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)). A thing
|
||||||
|
several of which coexist without contention — editors, browsers, media players — is not a seat;
|
||||||
|
each is a module with its own tools, and nothing is singular about it. A seat is held by one
|
||||||
|
assignment per node; other modules of the same family may be installed beside it without
|
||||||
|
holding it ([ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md) §1, read with the sharper
|
||||||
|
distinction: *installed* is not *holding*).
|
||||||
|
|
||||||
|
## Candidate seats
|
||||||
|
|
||||||
|
| seat | holders | gated by | first verbs |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **login shell** | zsh, fish, bash | nothing: universal | `execute(command)`; `show-config`; the holding itself sets the account's login shell through the host's `user` shape |
|
||||||
|
| **service manager** | systemd | the `service-manager` capability the profile reports | units: list, status, start, stop, restart, enable, journal; **user scope** on each |
|
||||||
|
| **boot** | grub, systemd-boot | a machine that boots itself (not a container host) | `rebuild-images`; `entries` |
|
||||||
|
| **package manager** | pacman, apt | the `package-manager` capability | search, installed, upgrade, orphans; today a capability the host uses, not a seat anyone holds |
|
||||||
|
| **display server** | xorg, wayland compositors that are their own server | the `graphical-session` capability | `displays`; `layout` |
|
||||||
|
| **display session** | i3, sway | the display server seat held on the node; i3 needs x11, sway needs wayland | `reload`; `workspaces`; `windows`; `move` |
|
||||||
|
| **terminal emulator** | xterm, alacritty, foot | display session | `open`; `font` |
|
||||||
|
| **launcher** | rofi, dmenu | display session | `show`; `theme` |
|
||||||
|
| **notifier** | dunst, mako | display session | `send`; `history`; `rule` |
|
||||||
|
| **compositor** | picom | display server (x11 only) | `restart`; `effects` |
|
||||||
|
| **lock screen** | i3lock, swaylock | display session | `lock` |
|
||||||
|
| **bar** | i3status-rust, waybar | display session | `reload`; `blocks` |
|
||||||
|
| **login manager** | lemurs, greetd | graphical session | `sessions`; `default-session` |
|
||||||
|
| **audio** | pipewire, pulseaudio | the machine reports a sound device | `sinks`, `sources`, `default`, `volume`, `mute` |
|
||||||
|
| **clipboard** | greenclip, cliphist | display session | `history`; `clear` |
|
||||||
|
|
||||||
|
Not seats, modules with their own tools: the editor, the browser, the mail client, the file
|
||||||
|
manager, the media player, the chat client, the agent at the terminal, the downloads folder, the
|
||||||
|
scripts folder, the sync client, the power and thermal daemons that are specific to one machine's
|
||||||
|
hardware.
|
||||||
|
|
||||||
|
## What the table implies
|
||||||
|
|
||||||
|
**Capabilities come first.** `graphical-session`, `service-manager` and `package-manager` are
|
||||||
|
reported today. *A display server is held* is not a capability but a seat being held, and a
|
||||||
|
module that needs it declares a dependency on the seat, not on a capability: *i3 needs the
|
||||||
|
display server seat held by xorg*. Whether a held seat can gate another's assignment is a
|
||||||
|
question for the controller's resolver, and the first environment module after the shell will
|
||||||
|
ask it.
|
||||||
|
|
||||||
|
**The service manager comes early.** Four of the predecessor's modules ship user units, and the
|
||||||
|
executor itself is a unit. User scope on the host's `service` shape is a host change whichever
|
||||||
|
module holds the seat; the seat's holder answers the questions about units, it does not apply
|
||||||
|
them — the host does, as it does for every declared resource.
|
||||||
|
|
||||||
|
**The shell comes first.** Universal, no capability, one verb that is immediately useful on
|
||||||
|
every node, and the `user` shape already makes the login shell declared state. It is the module
|
||||||
|
that proves the pattern: a package, files under the home owned by the account, a seat claim,
|
||||||
|
tools served by the executor, settings for the few things that vary per node, and a kept region
|
||||||
|
for the operator's own lines.
|
||||||
|
|
||||||
|
**The login manager is the first system-scope one**, because it needs nothing new: a package,
|
||||||
|
two files under `/etc`, a service — the same shape the ssh daemon module has today — and the
|
||||||
|
session script it owns is the file that was hand-fixed the day this effort opened.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
status: active
|
||||||
|
initiated: 2026-10-02
|
||||||
|
touches: [lab, the lab module, the catalogue, assignments, settings, the controller's store]
|
||||||
|
became: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# 019 — A warm twin of the running mesh
|
||||||
|
|
||||||
|
## What is being investigated
|
||||||
|
|
||||||
|
Whether the lab can keep a **warm twin of the mesh as it actually runs**: the same machines, carrying
|
||||||
|
the same catalogue, the same assignments and the same settings as the live mesh, raised once and kept
|
||||||
|
ready, so that a change can be tested against the mesh as it is rather than against a scenario
|
||||||
|
written to resemble it. A run against the twin would go through the lab module like any other run:
|
||||||
|
a branch per repository, the twin restored from its snapshot, the change applied, the beds run.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The lab's beds raise meshes from declarations written for the bed. They prove the mechanism. They
|
||||||
|
do not prove that a change works on the mesh that runs, with its accumulated assignments, its
|
||||||
|
operator settings, its adopted machines and its modules in their real combinations. The gap showed
|
||||||
|
on 2026-10-02:
|
||||||
|
|
||||||
|
- a change to how a module's settings reach its files was correct in every bed, and would have put a
|
||||||
|
setting into the container runtime's configuration on every machine running that module. Only the
|
||||||
|
composed plan for a real machine showed it;
|
||||||
|
- a firewall change composed cleanly and still left one machine's wired port unfiltered, because
|
||||||
|
of a link that machine had and no bed did;
|
||||||
|
- a recovery step was needed on every machine at once, after a change that every bed had passed.
|
||||||
|
|
||||||
|
The lab already has a warm mode, a snapshot of a raised scenario restored between attempts. What it
|
||||||
|
does not have is a scenario that **is** the running mesh, kept current with it.
|
||||||
|
|
||||||
|
## What it touches
|
||||||
|
|
||||||
|
- **What a twin is made of.** The catalogue and the assignments are records; settings are records;
|
||||||
|
secrets are sealed to machines and cannot be copied. Which of these can be carried to the lab as
|
||||||
|
they are, which must be substituted, and how a twin says what it substituted.
|
||||||
|
- **Data.** A twin with the real catalogue and no real data proves composition and delivery, not a
|
||||||
|
migration. Whether a twin carries data, a sample of it, or none.
|
||||||
|
- **Keeping it current.** A twin raised once goes stale with the first merge. Whether it is
|
||||||
|
re-derived from the live records on each run, refreshed on a schedule, or rebuilt only when asked.
|
||||||
|
- **Machines.** The live mesh has machines of different kinds: a server on the internet, machines
|
||||||
|
behind a home router, a laptop that sleeps. Which of their properties a twin must reproduce for a
|
||||||
|
test to mean anything (reachability, the private network, the found firewall).
|
||||||
|
- **Cost.** The lab machine's memory and disk, and how long a twin takes to raise from cold.
|
||||||
|
- **The lab module's tools.** A run against the twin rather than a named bed: one more tool, or an
|
||||||
|
argument to the run tool.
|
||||||
|
|
||||||
|
## Starting point
|
||||||
|
|
||||||
|
The lab module (ADR 0172) runs beds through the mesh, and the lab's warm mode already snapshots and
|
||||||
|
restores a raised scenario. The beds that raise a machine shaped like one live machine from the
|
||||||
|
catalogue are the nearest existing thing, and the first to compare against.
|
||||||
@@ -78,6 +78,8 @@ capability. The host hardcodes no firewall, supervisor, package manager or runti
|
|||||||
generic apply primitives and platform detection, so it runs where none of those exist — an Android
|
generic apply primitives and platform detection, so it runs where none of those exist — an Android
|
||||||
phone has no ufw, systemd, pacman or Docker.
|
phone has no ufw, systemd, pacman or Docker.
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-02, by [ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md).** The shell example above — *bash, zsh and fish all join `shell`; one may be default* — is read as *installed is not holding*: the three may all be installed, and the `login-shell` seat is node-scoped and held by exactly one. The decision — what a module is, and the three relationships — stands; [ADR 0173](0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md) applies it to the operator's whole machine.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- **Supersedes the earlier "grouped by domain" decision** (folded in consolidation; see the
|
- **Supersedes the earlier "grouped by domain" decision** (folded in consolidation; see the
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ extends: 0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md
|
|||||||
|
|
||||||
# 47. A module runs its code as its own process, with its own account
|
# 47. A module runs its code as its own process, with its own account
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** A module's *tools* are no longer served by the module's own process under its own account: one tool runtime per node, on the host side, serves every assigned module's bundle. A tool is still served on its own subject and only the module that serves it answers; what moved is the process and the account.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
A module is one self-contained thing ([ADR 0040](0040-what-a-module-is.md)), and it gets a broker
|
A module is one self-contained thing ([ADR 0040](0040-what-a-module-is.md)), and it gets a broker
|
||||||
|
|||||||
@@ -106,3 +106,14 @@ is refused with the candidates named, never resolved by picking.
|
|||||||
gap, and the day-one evidence.
|
gap, and the day-one evidence.
|
||||||
- [`03-DESIGN/01-to-be/23-choosing-a-provider.md`](../03-DESIGN/01-to-be/23-choosing-a-provider.md)
|
- [`03-DESIGN/01-to-be/23-choosing-a-provider.md`](../03-DESIGN/01-to-be/23-choosing-a-provider.md)
|
||||||
— the design.
|
— the design.
|
||||||
|
|
||||||
|
> **Widened, 2026-10-01 (issue #258).** The pin named a node, on the reasoning that "the same
|
||||||
|
> module on two machines is two answers, and which machine is the whole question". Half right:
|
||||||
|
> two modules on one machine can both answer a provision — `public-acme` and `step-ca` both offer
|
||||||
|
> `acme-ca` on novox — and then which *module* is the whole question, and a node alone cannot ask
|
||||||
|
> it. A provider is a (node, module) pair ([design 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md)),
|
||||||
|
> and a pin now names the pair: `pin <node> <provision> <from-node> <module>`. The resolver
|
||||||
|
> refuses a node that answers twice instead of taking the last one listed, and refuses two
|
||||||
|
> providers beside the consumer instead of settling them by a map walk — the same stance design 23
|
||||||
|
> takes: ambiguity is refused, never resolved by picking. Records made before are completed by
|
||||||
|
> migration where the node they name answers once. mesh-controller: `feat/pin-names-the-provider`.
|
||||||
|
|||||||
@@ -163,6 +163,14 @@ value, the requirement is marked not rotatable by the mesh, and a rotation is re
|
|||||||
**The number of parties decides, never the provider.** The resolver knows it from the requirement's
|
**The number of parties decides, never the provider.** The resolver knows it from the requirement's
|
||||||
recipients, leaving out the vault's custody copy, so no definition declares it.
|
recipients, leaving out the vault's custody copy, so no definition declares it.
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-01.** The number of parties is the resolver's to know; *which form*
|
||||||
|
> a single party's credential takes is not, and cannot be: whether a module reads its secret when it
|
||||||
|
> starts or applies it once to a backend is a fact about the software, visible nowhere in the graph.
|
||||||
|
> So the definition declares that half — `taken: at-start` or `taken: applied` on an own secret — and
|
||||||
|
> a secret that declares neither is not rotated, refused with the word to write (issue 180). The
|
||||||
|
> read-at-start form is built; the staged form for an applied credential is not. The decision stands;
|
||||||
|
> the sentence above was one fact short.
|
||||||
|
|
||||||
### Until an adapter can
|
### Until an adapter can
|
||||||
|
|
||||||
**An adapter that cannot yet ensure a second credential says so.** The two-party credentials it applies
|
**An adapter that cannot yet ensure a second credential says so.** The two-party credentials it applies
|
||||||
|
|||||||
+3
-1
@@ -81,7 +81,9 @@ closed set stays what its name says it is: the *system's* roles, not everyone's.
|
|||||||
- **`the-uplink` → `node-uplink`** ([ADR 0125](0117-a-machines-uplink-is-a-seat.md)). Unheld, so it
|
- **`the-uplink` → `node-uplink`** ([ADR 0125](0117-a-machines-uplink-is-a-seat.md)). Unheld, so it
|
||||||
renames with no migration.
|
renames with no migration.
|
||||||
- **The registry seats — `the-artifact-store`, `npm-package-registry` (→ `mesh-artifact-store`,
|
- **The registry seats — `the-artifact-store`, `npm-package-registry` (→ `mesh-artifact-store`,
|
||||||
`mesh-npm-package-registry`) — and `git` (→ `mesh-git`) — are decided but deferred.** They each
|
`mesh-npm-package-registry`) — and `git` (→ `mesh-git`) — are decided but deferred.** *(The
|
||||||
|
mechanism changed — 2026-09-30, by ADR 0156: `the-artifact-store` is renamed, one update and one
|
||||||
|
alias under ADR 0122; the other two stay deferred.)* They each
|
||||||
*deliver* a provision, so renaming them is a delivering-seat migration: a holder that stops
|
*deliver* a provision, so renaming them is a delivering-seat migration: a holder that stops
|
||||||
resolving mid-flight takes a provision away from every consumer. That risk is not worth carrying in
|
resolving mid-flight takes a provision away from every consumer. That risk is not worth carrying in
|
||||||
the same pass as the node-* renames, so they keep their names until done deliberately.
|
the same pass as the node-* renames, so they keep their names until done deliberately.
|
||||||
|
|||||||
@@ -124,6 +124,32 @@ This corrects a fact, not the decision: one statement per endpoint, three things
|
|||||||
none of them deciding on its own, all stand. The table in the decision should be read with the filter
|
none of them deciding on its own, all stand. The table in the decision should be read with the filter
|
||||||
column applying to an unrouted endpoint.
|
column applying to an unrouted endpoint.
|
||||||
|
|
||||||
|
## Progressive insight — 2026-10-02, from issue 191
|
||||||
|
|
||||||
|
**For a routed endpoint, "the proxy serves the internal name" has to mean "serves it to the private
|
||||||
|
network", and only the proxy can make it mean that.** The decision says `internal` means the proxy
|
||||||
|
serves the internal name and not the public one. It does not say to whom, and the proxy answered
|
||||||
|
every name it routes to any request that carried it, on the same listeners as its public names. A
|
||||||
|
name being internal kept nobody out: a request from the internet only had to send it. While every
|
||||||
|
routed endpoint also had a public name, nothing showed it. Once an endpoint could be internal alone
|
||||||
|
([issue 191](../04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/00-report.md)), serving
|
||||||
|
its name to everyone would have published exactly what `internal` was chosen to keep private.
|
||||||
|
|
||||||
|
The earlier insight above says the port is not the path for a routed endpoint. This is its other
|
||||||
|
half: the proxy is the path, so the proxy is where `internal` is enforced. It serves an internal name
|
||||||
|
only to the machines of the mesh and to the machine itself
|
||||||
|
([ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md)). Who the mesh is, it is told,
|
||||||
|
not left to work out: its membership carries the same list of machine addresses the filter's "from the
|
||||||
|
mesh" is rendered from ([ADR 0167](0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md)).
|
||||||
|
To anyone else, the name is answered as one never routed, in the handshake and in the request, and not
|
||||||
|
listed among the names it serves. This holds for the internal name of a `both` endpoint too, whose
|
||||||
|
outsiders have its public name.
|
||||||
|
|
||||||
|
The decision, the options and the consequences stand: one statement per endpoint, three things
|
||||||
|
derived from it. Checked in the proxy's own tests: an internal-only name is served to a machine the
|
||||||
|
membership names and to loopback, and refused, unlisted and uncertified for any other request; until
|
||||||
|
the mesh is issued, it is served to the machine alone.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- **A manifest gains endpoint names, and a route contribution names an endpoint instead of a port.**
|
- **A manifest gains endpoint names, and a route contribution names an endpoint instead of a port.**
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ 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
|
# 150. A module's own code runs as supervised processes under the module's one account
|
||||||
|
|
||||||
|
> **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
|
## Context
|
||||||
|
|
||||||
The repository answers "what runs a module's own code" two ways and reconciles them nowhere
|
The repository answers "what runs a module's own code" two ways and reconciles them nowhere
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ operator owns; narrowing what it may call is a setting on its assignment, which
|
|||||||
[ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) already provides for
|
[ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) already provides for
|
||||||
and nothing here builds.
|
and nothing here builds.
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** The surface stays a module assigned per node, on loopback, with the machine's login as the authority. It is no longer a container: it is the node tools runtime's serving mode, host-side, and that runtime also serves every assigned module's tools. The module is renamed `node-tools`.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- **The way a person drives the mesh is inside the mesh.** It is declared, delivered, replaced and
|
- **The way a person drives the mesh is inside the mesh.** It is declared, delivered, replaced and
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ itself restarts, and the console says so rather than hiding the modules' tools w
|
|||||||
**A grant of `*` reaches a role's tools; `seat:<seat>.<verb>` grants one.** The console's `*` needed no
|
**A grant of `*` reaches a role's tools; `seat:<seat>.<verb>` grants one.** The console's `*` needed no
|
||||||
change to reach the mesh's verbs, which is what a grant meaning *every tool* should mean.
|
change to reach the mesh's verbs, which is what a grant meaning *every tool* should mean.
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).**
|
||||||
|
> The seat gains a generic verb beside the named ones: `command`, which takes one command line as the
|
||||||
|
> controller's binary takes it and answers what it printed. The named verbs stand and keep their
|
||||||
|
> schemas; `command` is the whole binary, added because the operator decided any node may call any
|
||||||
|
> tool and a verb per command was the only thing keeping `node account`, `node show` and the rest
|
||||||
|
> behind a shell on the control node. Additive within the version, as §"additive" above allows.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- **The console answers the mesh's own questions.** Issue 147's first paragraph closes: what a node
|
- **The console answers the mesh's own questions.** Issue 147's first paragraph closes: what a node
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-30
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 155. A definition names no installation: how that is checked, and the three ways a value that did gets out
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) decided that a module definition
|
||||||
|
names no node, no mesh and no host path, and said how the name half is checked: *a catalogue test
|
||||||
|
finds no domain name in any definition value*. No such test existed
|
||||||
|
([issue 134](../04-ISSUES/134-a-definition-may-still-name-the-mesh/00-report.md)). Written and run
|
||||||
|
over the 77 definitions on 2026-09-30, the check it describes finds **42 values**, in 15 definitions,
|
||||||
|
and they are of four kinds that want four different answers:
|
||||||
|
|
||||||
|
| kind | count | example |
|
||||||
|
|---|---|---|
|
||||||
|
| a service told its own public name as a literal | 5 | an identity provider's `KC_HOSTNAME`, an object store's console redirect, an automation tool's webhook URL |
|
||||||
|
| an operator's value written into the definition | 10 | a mail server's domain, site name, website, and the address it trusts a real-IP header from |
|
||||||
|
| this mesh's forge, by URL, as a recipe's build context | 2 | the builder and the proxy, which package the controller's source |
|
||||||
|
| an application built outside the mesh, pulled from this mesh's registry | 7 | four sites and tools whose repositories are the operator's own |
|
||||||
|
| the world's servers, named by upstream defaults | 10 | a Matrix homeserver's trusted key server, Element's integration manager |
|
||||||
|
| a module named after the domain it serves | 8 | one site module, with its paths and network named after it |
|
||||||
|
|
||||||
|
Not one was careless. Each was the value the software needs, and until today there was nowhere else
|
||||||
|
to put it ([issue 122](../04-ISSUES/122-a-module-cannot-ask-for-its-own-public-name/00-report.md)).
|
||||||
|
Two of the answers were built before this record: a module is told the name its route composes
|
||||||
|
(`${bound:<route>:name}`, controller PR 149, 2026-09-30), and a source may be a path on the git seat
|
||||||
|
([ADR 0111](0111-a-build-source-is-on-the-git-seat-or-external.md)). What was missing: an operator's
|
||||||
|
value in a file the software reads, the same for a build *context*, a way to say a name is meant, and
|
||||||
|
the check.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**1. A string search for the installation's own names.** Rejected. The controller is as
|
||||||
|
mesh-agnostic as the definitions; it does not know which names are "this mesh's", and a check that had
|
||||||
|
to be told would be configured per installation and pass everywhere else. What it can know is the
|
||||||
|
*shape*: a name under a public top-level domain, a public address.
|
||||||
|
|
||||||
|
**2. Report every such shape.** Rejected. Eight of the 42 were `why` strings — prose the mesh never
|
||||||
|
reads, explaining what a port is for — and a check that reports those beside `KC_HOSTNAME` teaches
|
||||||
|
people to ignore the report. And a Matrix homeserver *must* name the federation's public key server;
|
||||||
|
a check with no way to say so would be a check people argue with rather than obey.
|
||||||
|
|
||||||
|
**3. Judge what the mesh acts on; let a definition say which names it means, one by one, with a
|
||||||
|
reason; exempt the world's services that a definition may name as a policy default.** Chosen.
|
||||||
|
|
||||||
|
**For an operator's value**, one option was to wait for design 27's requirement form in full. Rejected
|
||||||
|
for the reason 0112 gave against a slow operator provider: if asking a person for a value takes more
|
||||||
|
than a setting, module authors route around it and the literals come back. `${setting:<key>}` is the
|
||||||
|
operator provider in its first form, on the settings a module already has.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**The check.** Every string value of a definition that the mesh acts on is judged for a hostname under
|
||||||
|
a public top-level domain and for a public address. Not judged: `why` and `description`, which are
|
||||||
|
prose. Allowed where they can only mean the world: the public registries an `image` may be pulled
|
||||||
|
from, the public resolvers a machine may forward to, and the public certificate authorities' ACME
|
||||||
|
directories. The container runtime's alias for its own host is the runtime's. A module's own name is a
|
||||||
|
value too. The check runs in `module check` and as a catalogue-wide test; **it does not yet refuse at
|
||||||
|
registration**, because the list it prints is the list that shrinks, and a registration that refused a
|
||||||
|
manifest whose only remedy is a merge elsewhere would refuse the mesh's own catalogue on the day the
|
||||||
|
check landed. It moves to registration when the list has been empty for a release.
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-09-30.** The list was empty the day the check landed — every remaining name declared with its reason — and the operator asked for registration to refuse at once rather than after a release. It does, since mesh-controller PR 175: `module add` and a build's result are refused in the check's words, naming the way out, and the build stays recorded. The decision stands; only the day moved.
|
||||||
|
|
||||||
|
**A name a definition means is declared with its reason.** `names-on-purpose` on a resource maps each
|
||||||
|
such name to why: *the federation's public key server, the world's*; *built outside the mesh, from the
|
||||||
|
application's own repository, until that repository is a build source on the git seat*. A name the map
|
||||||
|
does not cover is still reported. The host never sees the word.
|
||||||
|
|
||||||
|
**An operator's value reaches a file as `${setting:<key>}`**, filled from the module's settings
|
||||||
|
layers — the mesh's, then the node's — the same layers a mergeable file and a contribution take, so
|
||||||
|
`settings set <module>` stays the one place a person's values go. Refused, naming the key and the
|
||||||
|
command, when nothing set it: a default for a mail domain would be the literal this removes, and a
|
||||||
|
blank written silently would be a service that comes up wrong somewhere that names nothing.
|
||||||
|
|
||||||
|
**A build context may live on the git seat.** `context: {"seat": "git", "repository": "<owner>/<name>"}`
|
||||||
|
is composed by the mesh that builds it: the request carries each seat's clone base, and a builder told
|
||||||
|
no base for a seat a context names refuses the build by the seat's name rather than guessing a forge.
|
||||||
|
|
||||||
|
**A module is named for what it is.** The site module named after its domain is `website`.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **The catalogue names no installation, and a test says so.** The forty-two became zero the same day,
|
||||||
|
by the four answers above; seven of them are declared on purpose and stay visible as the list to
|
||||||
|
shrink — four applications the mesh does not build yet.
|
||||||
|
- **An operator's values are the assignment's.** The mail module takes its domain, its site name, its
|
||||||
|
website and the address it trusts a real-IP header from as settings; a mesh that installs it without
|
||||||
|
them is refused at composition, by name, which is the right moment. The module's own README says
|
||||||
|
which.
|
||||||
|
- **What got harder:** a manifest reviewer has one more word to read, and `names-on-purpose` on an
|
||||||
|
application's image is a debt visible in the definition until the application is built here. A
|
||||||
|
reader of `settings set` output sees more keys than files, because a key a file asks for is a
|
||||||
|
destination too.
|
||||||
|
- **Not decided here:** ADR 0112's requirement form (design 27) still replaces `${setting:…}` and the
|
||||||
|
other placeholders when it lands; this is its first case, the way [ADR 0038](0038-the-mesh-assigns-the-port.md)
|
||||||
|
was for ports. Host paths ([issue 119](../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md))
|
||||||
|
are the next step of the same group, and the registry's name ([issue 123](../04-ISSUES/123-the-image-registry-is-named-after-a-role/00-report.md))
|
||||||
|
the one after.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A value naming an installation is reported at its path, in the definition's words | a catalogue unit test over a definition with a hostname in an env value and a public address in a file |
|
||||||
|
| Prose, the world's registries in an image, public resolvers, ACME directories and the runtime's own alias are not reported | the same tests |
|
||||||
|
| A name declared on purpose is not reported; a name beside it that is not declared is | a test with a homeserver's config |
|
||||||
|
| An image from an installation's registry needs a reason | a test without and with the word |
|
||||||
|
| A module named after a domain is reported | a test |
|
||||||
|
| No definition in the catalogue names an installation | `TestNoCatalogueManifestNamesAnInstallation` over the checkout, and `module check modules/` |
|
||||||
|
| A definition naming an installation is refused at registration, and one declaring its names passes | `TestRegistrationRefusesADefinitionNamingAnInstallation` (2026-09-30) |
|
||||||
|
| `${setting:key}` fills from the layers, node over mesh; refused by name when unset; not stray when set | three tests |
|
||||||
|
| A context on a seat is cloned from the base the mesh sent; a seat with no base is refused by name | the builder's test |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) — extended: the check it promised, and the operator provider's first form
|
||||||
|
- [ADR 0111](0111-a-build-source-is-on-the-git-seat-or-external.md) — a source on the seat; now a context too
|
||||||
|
- [issue 122](../04-ISSUES/122-a-module-cannot-ask-for-its-own-public-name/00-report.md), [issue 134](../04-ISSUES/134-a-definition-may-still-name-the-mesh/00-report.md) — what this closes
|
||||||
|
- [27 — A module requires, the mesh resolves](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md) — where this sits in the larger design
|
||||||
|
- mesh-controller PR 169, mesh-catalog PR 188 — the check, the words, and the catalogue that passes it
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-30
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0075-two-stores-and-which-provides-what.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 156. An artifact is what a build produces, the artifact store serves every kind, and its seat is named for its scope
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[Issue 123](../04-ISSUES/123-the-image-registry-is-named-after-a-role/00-report.md) found three
|
||||||
|
wordings disagreeing about the mesh's registry. The glossary defined *artifact* as "an OCI image, by
|
||||||
|
digest"; the manifest's build vocabulary names four kinds — `image`, `upstream`, `bundle`, `archive` —
|
||||||
|
and the catalogue builds all four; the seat was `the-artifact-store`, the last of the mesh's own seats
|
||||||
|
named after the job it does rather than for the mesh
|
||||||
|
([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) decided the
|
||||||
|
rename and deferred it). The issue asked whether the seat and provision should be renamed after
|
||||||
|
images, and whether the mesh needs two registry implementations at all.
|
||||||
|
|
||||||
|
Reading what the store actually serves settles the first question the other way. A kept reference
|
||||||
|
has two shapes — `artifact-store://<module>/<artifact>@sha256:…` for an image and
|
||||||
|
`artifact-store://<module>/<artifact>/blobs/sha256:…` for an archive — and both are served by the
|
||||||
|
same OCI registry, by digest. [ADR 0075](0075-two-stores-and-which-provides-what.md) already
|
||||||
|
defined the provision that way: *content-addressed blobs, pinned by digest, no versions, no ranges;
|
||||||
|
what the mesh delivers to machines*. The provision was never an image registry. Only the glossary said
|
||||||
|
so, and only the seat's name was odd.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**1. Rename the seat and the provision after images.** Rejected. The store serves archives too, by the
|
||||||
|
same protocol; naming it for one kind would be the glossary's mistake made permanent, in the name
|
||||||
|
every manifest uses.
|
||||||
|
|
||||||
|
**2. Fix the word, rename the seat for its scope, keep the provision.** Chosen. The rename ADR 0121
|
||||||
|
deferred as a delivering-seat migration is, since [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md),
|
||||||
|
one update and one alias: the former name resolves forever, a held record follows by cascade, a claim
|
||||||
|
written with the old name still holds.
|
||||||
|
|
||||||
|
**On two implementations:** left as 0075 decided. Two provisions because two protocols; the OCI
|
||||||
|
registry the genesis installs because something must serve images before the mesh can build; the
|
||||||
|
forge may provide `artifact-store` too and a mesh may choose it. The bootstrap argument is weaker than
|
||||||
|
it reads, as 123 says, and the day the forge is raised at genesis and adopted in place is the day to
|
||||||
|
retire the second server — a migration a mesh performs, not a decision to take here.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
- **An artifact is anything a build produces** — an image, a mirrored upstream image, a bundle, an
|
||||||
|
archive — and the glossary says so. *Image* is one kind. A module is not an image; a module may
|
||||||
|
build several artifacts and install none.
|
||||||
|
- **The artifact store serves artifacts of every kind a machine fetches**, images and archives, by
|
||||||
|
digest, over the OCI registry protocol. The provision keeps its name.
|
||||||
|
- **The seat is `mesh-artifact-store`.** `the-artifact-store` is its alias. The catalogue's registry
|
||||||
|
module claims the new name; a definition elsewhere claiming the old one still holds.
|
||||||
|
- The two other deferred renames — `npm-package-registry` and `git` — stay deferred, and for the
|
||||||
|
same reason no longer. They are one migration each when wanted; nothing here needs them.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The glossary stops contradicting the manifest vocabulary, and a reader of `artifact-store` reads
|
||||||
|
it as what it is: where the mesh's built things are kept.
|
||||||
|
- One migration on the seat table; no manifest but the registry's changes; no consumer of the
|
||||||
|
provision changes, because the provision did not.
|
||||||
|
- **What got harder:** nothing measurable. A record that says `the-artifact-store` is read through the
|
||||||
|
alias; design 26's table already carried the new name as intent.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| The former name resolves to the seat once the store's aliases are loaded | a catalogue test |
|
||||||
|
| The seat is in the compiled set under its new name, delivering `artifact-store` | the seat tests, updated |
|
||||||
|
| The registry module holds the seat under the new name on the live mesh | `seats` after the rollout |
|
||||||
|
| The glossary's *artifact* matches the build kinds a manifest may declare | design 18's table and the showcase module list the kinds; the glossary names the same four |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [issue 123](../04-ISSUES/123-the-image-registry-is-named-after-a-role/00-report.md)
|
||||||
|
- [ADR 0075](0075-two-stores-and-which-provides-what.md) — extended: the provision as defined stands, the word is corrected
|
||||||
|
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md) — the rename, decided and made cheap
|
||||||
|
- mesh-controller migration 0048; mesh-catalog `modules/distribution`
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-01
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 157. A build says what it does on the bus, as it happens
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) made a build work submitted to a role: the
|
||||||
|
build-machine seat accepts a build and emits its outcome, one publish that reaches whoever asked, the
|
||||||
|
controller that records it and the catalogue that places it. Everything **between** the request and
|
||||||
|
the outcome — which command is running, how long it has taken, where it hung, the compiler's error,
|
||||||
|
the clone's refusal — lived in one container's standard error on one machine.
|
||||||
|
|
||||||
|
The night of 2026-09-30 showed the cost three times over. A build that failed showed a person one
|
||||||
|
line, the first of its failure, in the controller's `builds`; the rest was read with `docker logs` over
|
||||||
|
ssh, which the mesh's own rule forbids. A build that ran for minutes could not be told from one that
|
||||||
|
had hung. And the builder has no tools and emits nothing but the outcome, so the console
|
||||||
|
([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)) had nothing to show while a
|
||||||
|
build ran, and no viewer could be built on top of it. The operator's ask was plain: the builder is to
|
||||||
|
be fully transparent, with its log on the bus, so that a log viewer can be built on the bus later.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep the log in the outcome.** The result carries the whole log when the build ends. Nothing new
|
||||||
|
on the bus; nothing while the build runs; a viewer sees a build only once it is over, which is
|
||||||
|
exactly when the log matters least.
|
||||||
|
2. **A log store.** The builder writes its log to a file or a table and a tool reads it. A second
|
||||||
|
place to keep something the bus already carries, with its own retention, access and failure modes,
|
||||||
|
and no live reading without inventing a subscription over it.
|
||||||
|
3. **The log is the role's own events.** Two more events on the build-machine seat beside `built`:
|
||||||
|
`started` when work is taken, and `log.<build id>` for every line, published as the build runs.
|
||||||
|
The events stream already retains every role's events for a week, so a reader follows a build
|
||||||
|
live by subscribing its subject, or reads it back afterwards from the stream, and a viewer is a
|
||||||
|
subscriber and nothing more.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Option 3.** A build machine says everything it does on the bus, as the role it holds, under the
|
||||||
|
build's id, and the mesh keeps no other copy.
|
||||||
|
|
||||||
|
- The build-machine seat's protocol gains `started` and `log.*`. A holder may therefore publish
|
||||||
|
`mesh.seat.mesh-build-machine.event.started` and `…event.log.<id>`, and no other subject, by the
|
||||||
|
same derivation every seat's grants follow ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
|
||||||
|
The event's tail token is the build's id, so one build is one subject: a reader filters by subject
|
||||||
|
alone, on the server, and a week of other builds does not travel to show one.
|
||||||
|
- **Every line goes two ways**: to the machine's own standard error as before, and onto the bus. That
|
||||||
|
includes every command the builder runs, its duration and its failure, and on failure the command's
|
||||||
|
own output line by line — the compiler's words, the clone's refusal. A build machine with nobody
|
||||||
|
listening still prints; a listener reads the same lines.
|
||||||
|
- A line is a core publish, unawaited. The stream that holds the role's events captures it on its way
|
||||||
|
through, and a build does not slow to the pace of an acknowledgement per line. Each line carries a
|
||||||
|
sequence number from one, so a reader who joined late, or reads two copies, sees order and gaps.
|
||||||
|
`started` and `built` are published into the stream and awaited, because they are the two facts a
|
||||||
|
later reader must never find missing.
|
||||||
|
- **The mesh reads it back from the stream**, never from a record of its own: `builds --log <id>`, and
|
||||||
|
the same verb on the controller's seat ([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)),
|
||||||
|
reads one build's subject with a consumer that is gone when the reading is done. `builds` lists
|
||||||
|
each build's id beside it, and `build` says the id it asked with, so a person can follow.
|
||||||
|
- Nothing is declared by the builder module for this. The protocol is the seat's, seeded additively
|
||||||
|
into the store ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)), and the
|
||||||
|
holder's grant follows on the next composition of the broker node.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- A build is watchable while it runs, from anywhere on the mesh, with no access to the build machine.
|
||||||
|
The console's gap of 2026-10-01 — no live progress, no per-merge view — closes on the progress half;
|
||||||
|
the per-merge view is a reader over these subjects and the outcome, and is not built here.
|
||||||
|
- A log viewer on the bus is now a plain subscriber: live on `mesh.seat.mesh-build-machine.event.>`,
|
||||||
|
historical from the events stream filtered by a build's subject. NATS carries and retains; it does
|
||||||
|
not view. The `nats` command-line client can tail or replay a subject today; a viewer of our own is
|
||||||
|
later work and needs nothing more from the builder.
|
||||||
|
- The events stream grows by a build's log per build, for a week. A build is a few hundred lines; the
|
||||||
|
stream's limits are the bound, as for every other event, and a stream that fills drops the oldest.
|
||||||
|
- A line the bus did not take is lost, deliberately, and visible as a gap in the sequence. The outcome
|
||||||
|
is not affected: a build's result never depended on its narration.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| The seat's holder may publish `started` and `log.<id>` and nothing wider | `TestTheBuildMachineMaySayWhatItDoesUnderTheBuildsId` (broker) |
|
||||||
|
| A build's lines reach a reader of its subject in order, and the stream holds them afterwards | `TestNatsABuildIsTakenAndItsOutcomeReachesEverybody` against a real server (link) |
|
||||||
|
| The seat verb `builds` with a build's id reads that build's log | `TestBuildsWithAnIdReadsThatBuildsLog` |
|
||||||
|
| Every command the builder runs is said, with its output on failure | `Command` speaks through the hook every build sets; the builder's tests still see the lines on standard error when nothing listens |
|
||||||
|
| Live: a build triggered after the roll-out is readable line by line through the console | done by hand after the merge of mesh-controller PR — see the design's note |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) — extended: the role now narrates as well as answers
|
||||||
|
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md) — the protocol and the verb
|
||||||
|
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — the console this feeds
|
||||||
|
- [Design 25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §3, [Design 18 — Building a module](../03-DESIGN/01-to-be/18-building-a-module.md)
|
||||||
|
- [Issue 176](../04-ISSUES/176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md) — the tool that starts a build and hears nothing; this gives it something to hear
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-01
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 158. A provider with one credential shares it with every consumer, and the vault remakes it for all of them at once
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) gave every consumer of a
|
||||||
|
provision its own credential: the mesh mints one per pair, the provider's own code creates the
|
||||||
|
login, and rotating one consumer's touches nothing else. That is right for a database, a broker, an
|
||||||
|
object store — software that can hold many logins.
|
||||||
|
|
||||||
|
The media software on the home server cannot. A download client has one web password; an indexer
|
||||||
|
has one API key; each of the library managers has one key in its configuration; the media server
|
||||||
|
holds one token issued elsewhere. There is no login per consumer to create, so
|
||||||
|
[ADR 0113](0113-the-vault-makes-every-secret.md)'s only remaining form applied: the value is
|
||||||
|
*accepted*. On 2026-10-01 the home server held forty-seven accepted own secrets and twelve accepted
|
||||||
|
pair credentials, every one rotatable only by a person changing the software by hand and accepting
|
||||||
|
the new value, and one pair credential sat *made* and wrong because nobody could accept the real one.
|
||||||
|
The operator asked for every password in the vault and rotatable, and for a library manager's
|
||||||
|
definition to receive the download client's credential and address through provisioning like
|
||||||
|
anything else (filed as the forge's issue 243 on this repository).
|
||||||
|
|
||||||
|
The address half already works: the library manager requires the download client's API provision,
|
||||||
|
the provider serves scheme, port and user name, and the binding carries them. Only the credential
|
||||||
|
half had no form.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep accepting.** Honest about what the software can do and what the mesh cannot, and it is
|
||||||
|
the state the home server was in: nothing rotates, a consumer added later needs a person, and an
|
||||||
|
unknown predecessor password stays unknown for ever.
|
||||||
|
2. **Put a login per consumer in front of the software.** A proxy that holds the one credential and
|
||||||
|
issues many. A second service per provider, with its own credential to keep, to make the mesh's
|
||||||
|
model fit software that does not share it.
|
||||||
|
3. **Let the provider say its one credential is the credential.** An offer names which of the
|
||||||
|
provider's own secrets *is* what every consumer receives. The vault keeps one record, sealed to
|
||||||
|
the provider's machine, every current consumer's machine and the operator, and because it stores
|
||||||
|
no plaintext it cannot seal an existing value to a later consumer — so it **remakes the value
|
||||||
|
for all of them at once** whenever the set of consumers changes or a rotation is asked. The
|
||||||
|
provider takes it the way an own secret is taken ([ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md),
|
||||||
|
issue 180); consumers read it at start.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Option 3.** A provider whose software holds one credential shares that credential, and the mesh
|
||||||
|
owns its whole lifecycle.
|
||||||
|
|
||||||
|
- **The offer says so.** `{"name": "download-client-api", "credential": {"own": "password"}}` on a
|
||||||
|
provider's `provides` entry names one of its own secrets as the credential of that provision. The
|
||||||
|
named own secret must say how it is taken (`taken: at-start` or `taken: applied`); an offer
|
||||||
|
naming an undeclared or untaken secret is refused at parse.
|
||||||
|
- **One record, many seals.** The vault keeps one value per (provider assignment, provision). It is
|
||||||
|
sealed to the provider's machine, to each consumer's machine that currently binds the provision,
|
||||||
|
and to the operator. Every consumer's binding file carries the provider's one user name and the
|
||||||
|
secret file carries the shared value; the shape a consumer reads is the pair credential's, so a
|
||||||
|
consumer's definition does not know whether its credential is shared.
|
||||||
|
- **Remade for all, together.** When a consumer binds or unbinds, or `secret rotate` is asked on the
|
||||||
|
provider's own secret, the vault makes a new value and seals it to every current holder in one
|
||||||
|
act, and the mesh sends every holding machine. The provider restarts on the new value or applies
|
||||||
|
it at start; each consumer restarts on it. There is no window between two credentials, because
|
||||||
|
there is one credential; there is the restart, stated as the cost below.
|
||||||
|
- **An accepted shared value is sealed to everyone the moment it is accepted.** `secret accept` on
|
||||||
|
the provider's own secret is the one moment the mesh holds the plaintext, and it seals copies for
|
||||||
|
every current consumer then. It is not remade afterwards ([ADR 0113](0113-the-vault-makes-every-secret.md)):
|
||||||
|
a consumer that binds later is refused until the value is accepted again, in words that say so.
|
||||||
|
- **A value the software issues itself stays accepted.** A token the media server obtains from its
|
||||||
|
vendor cannot be set by the mesh; its provision keeps the accepted form until a module can deliver a
|
||||||
|
value it did not mint to the vault, which this record does not build.
|
||||||
|
- **Nothing changes for software that holds many logins.** ADR 0048's form stays the default; this
|
||||||
|
is the form for an offer that says it has one credential.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The media stack's six providers stop needing a person per consumer. A library manager binding
|
||||||
|
the download client gets a working credential the mesh made, and an unknown predecessor password
|
||||||
|
is replaced by one the mesh knows, recoverable with the operator's key.
|
||||||
|
- **Adding or removing a consumer restarts every consumer of that provision and the provider.**
|
||||||
|
That is the price of one credential, and it is paid when a definition binds, not at an hour of
|
||||||
|
nobody's choosing. It is stated in the plan's words when it happens.
|
||||||
|
- Rotation of a shared credential is [ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md)'s
|
||||||
|
single-party form across several machines: in place, all holders sent together. The staged form
|
||||||
|
for a backend that takes its credential once is still not built, and a provider whose own secret
|
||||||
|
says `applied` refuses rotation by name until it is.
|
||||||
|
- The vault can name who holds a shared value — the copies are the record — so *who has this* stays
|
||||||
|
a query, as design 13 requires.
|
||||||
|
- The accepted count on the home server becomes a list that shrinks, provider by provider, as each
|
||||||
|
one's start applies the file.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| An offer may name one of its own secrets as its credential; an undeclared or untaken secret is refused at parse | manifest tests |
|
||||||
|
| A consumer of a shared provision receives the provider's value as its pair credential, under the provider's one user name | resolver and declaration tests |
|
||||||
|
| The record is sealed to the provider, every current consumer and the operator; a consumer binding or unbinding remakes it for all | inventory tests against a raised store |
|
||||||
|
| Rotating the provider's own secret remakes every holder's copy, and an accepted value is sealed to current consumers once and not remade | inventory tests |
|
||||||
|
| Live: a library manager on the home server binds the download client with a value the mesh made, the client takes it at start, and a rotation through the console reaches both | done by hand after the media catalogue's providers apply the file at start |
|
||||||
|
|
||||||
|
*2026-10-01:* the first four rows pass in mesh-controller PR 184 (`make check` green); the live row waits for the first provider definition to say `credential` and `taken`.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) — extended: the per-consumer form stays the default; this is the form for one credential
|
||||||
|
- [ADR 0113](0113-the-vault-makes-every-secret.md), [ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md) — the accepted form and the single-party rotation this rests on
|
||||||
|
- [Issue 180](../04-ISSUES/180-a-modules-own-secret-cannot-be-rotated/00-report.md) — the `taken` word and the rotation this reuses
|
||||||
|
- [Design 24 — The secrets vault](../03-DESIGN/01-to-be/24-the-secrets-vault.md), [Design 13 — Credentials and their rotation](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md)
|
||||||
|
- The forge's issue 243 on this repository, where the operator's ask and the home server's count were recorded
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-01
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 159. A tool call names the machine it is for, every answer says which machine answered, and a holder's runtime serves its seat's verbs
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) made a module's tools subjects on
|
||||||
|
the bus and the console the place a person reaches them. [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
|
||||||
|
made the mesh's own verbs the controller seat's tools, served by the controller. Design 33 said what
|
||||||
|
a seat's tools are, that holding a seat means serving them, and that a node-scoped seat's verb
|
||||||
|
carries the machine.
|
||||||
|
|
||||||
|
What was built stopped short in two places ([issue 182](../04-ISSUES/182-a-tool-call-reaches-whichever-instance-answers-first/00-report.md)).
|
||||||
|
A module's tools were one subject per module in one queue group, so with the database engine on two
|
||||||
|
machines a call reached whichever instance answered first, unnamed, and nobody could ask one machine's.
|
||||||
|
And no module served the verbs of a seat it held: the runtime did not know which seats its module
|
||||||
|
claimed, and no seat but the controller's declared verbs. The operator named it: a tool call must be
|
||||||
|
able to say *the store on the control node*, and the engine holding the store seat must serve the
|
||||||
|
store's tools as well as its own.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Leave the queue group and ask the controller which machine answered.** Nothing changes on the
|
||||||
|
bus; a caller cannot choose, only learn afterwards. Useless for the question that was asked.
|
||||||
|
2. **A subject per machine instead of one per module.** Every call names a machine; a stateless
|
||||||
|
module on three machines loses the one-of-them answer a queue group gives for free, and every
|
||||||
|
caller has to know where things run.
|
||||||
|
3. **Both subjects, and the machine in every answer.** An instance serves its module's subject in the
|
||||||
|
queue group as before, and the same subject with its machine as the last token. A caller that
|
||||||
|
names no machine gets one instance and is told which; a caller that names one gets that one. The
|
||||||
|
grant for a tool covers both. And a holder's runtime serves its seat's verbs by the same means,
|
||||||
|
from what the credential tells it.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Option 3.**
|
||||||
|
|
||||||
|
- **Two subjects per tool, one default.** `mesh.mod.<module>.tool.<tool>` in the queue group, and
|
||||||
|
`mesh.mod.<module>.tool.<tool>.<node>` served by the instance on that machine alone. In the
|
||||||
|
caller's words, `<module>.<tool>@<node>`. A runtime that does not know its machine serves only the
|
||||||
|
first, which is what it always did.
|
||||||
|
- **Every answer says which machine answered.** The reply carries the node; the console appends
|
||||||
|
*answered by <node>* as its own line after the module's unshaped answer, and `mesh call` prints it.
|
||||||
|
An answer from a module on several machines is never an answer from nowhere.
|
||||||
|
- **The console offers the machine on every module tool** as an optional `node` argument, lists it,
|
||||||
|
strips it into the subject and never passes it to the module. A seat's verb takes none: the seat's
|
||||||
|
scope decides where it is served.
|
||||||
|
- **The grant covers both subjects.** `invokes: [<module>.<tool>]` permits the plain subject and the
|
||||||
|
machine-addressed one; `*` already permitted everything beneath `tool`.
|
||||||
|
- **A holder's runtime serves its seat's verbs.** The broker credential the mesh writes names the
|
||||||
|
seats the module claims and, for each, its scope and the verbs the seat promises. The runtime
|
||||||
|
serves each verb with the module's tool of the same name on the seat's own subject — flat for a
|
||||||
|
mesh seat, with the machine for a node-scoped one — and the bus admits that subscription only
|
||||||
|
where the module holds the seat, because the holder's grant is composed from the holding. A
|
||||||
|
claimant that does not hold the seat here is refused the subscription and serves nothing. A
|
||||||
|
claimant missing a tool a seat promises is already refused at registration (design 33 §3).
|
||||||
|
- **The store seat's first verbs**, so the operator's question has an answer: `databases`, every
|
||||||
|
database the store holds with its owner and size, and `query`, one read-only statement against one
|
||||||
|
database. The database engine serves both as tools of those names and lists them in its definition.
|
||||||
|
Which verbs a seat serves is a decision per seat and binds every holder; these two are the smallest
|
||||||
|
set that makes the store askable.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- *List the databases of the store on the control node* is `mesh-store.databases` through the seat,
|
||||||
|
answered by its holder wherever it sits, or `postgres.databases@novox` through the module on one
|
||||||
|
named machine. Both say who answered.
|
||||||
|
- Every module's runtime serves one more subscription per tool and, for a claimant, one per promised
|
||||||
|
verb. No manifest changes for the per-machine half; the seat half needs each holder's definition to
|
||||||
|
list the seat's verbs among its tools, which registration already demands.
|
||||||
|
- The runtime change reaches a module when the module is rebuilt on the new runtime image; until
|
||||||
|
then that module answers only on its plain subject, and a call naming its machine is refused as
|
||||||
|
unserved, in words that say so.
|
||||||
|
- The credential gains `claims`; a module issued before this carries none and serves no seat verb
|
||||||
|
until it is issued again. `rollout mint` for the holders is the one-time cost.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A call naming a machine reaches that machine's instance; an unnamed call reaches one and says which | mesh-tools, against a real bus: a module on two machines |
|
||||||
|
| A claimant serves a seat's verb on the seat's subject, and the answer names the machine | the same test |
|
||||||
|
| The console lists `node` on a module's tool and not on a seat's verb, and the answer carries *answered by* | mesh-tools, the MCP conformance test |
|
||||||
|
| The grant for a tool covers the plain and the machine-addressed subject | `TestInvokingAToolMayAddressTheMachineToo` (controller) |
|
||||||
|
| Live: the store's databases listed from the control node by name through the console, and through the store seat | done by hand after the roll-out and the catalogue's step |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — extended: the surface carries the machine
|
||||||
|
- [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — the seat half, now for every holder
|
||||||
|
- [Design 33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §3, §4; [Design 34 — The console](../03-DESIGN/01-to-be/34-the-console.md) §3
|
||||||
|
- [Issue 182](../04-ISSUES/182-a-tool-call-reaches-whichever-instance-answers-first/00-report.md)
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-01
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 160. The mesh issues an assignment's subjects, and a runtime serves what it is issued
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
A module's code names no subject. It registers tools by name and emits events by name, and design 29
|
||||||
|
§1 says the rest: *the module names its event and the mesh decides where it lands*. What was built
|
||||||
|
decided it twice. The runtime derives `mesh.mod.<module>.tool.<name>` from the module's name by a rule
|
||||||
|
compiled into it; the controller derives the same subject by the same rule compiled into it, and grants
|
||||||
|
it. They agree because two binaries carry one convention, which is the failure design 33 §2 names for
|
||||||
|
seat protocols: *discovery that reads a binary disagrees with the mesh the moment the two are on
|
||||||
|
different versions*. [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
|
||||||
|
extended the convention this morning — a second subject per tool with the machine as its last token, a
|
||||||
|
seat's verbs served from the credential's claims — and extending it made the shape plain: every such
|
||||||
|
change is written in the runtime and in the controller, and a module whose instances must not be
|
||||||
|
confused is told apart by a rule in a binary rather than by the mesh that assigned it.
|
||||||
|
|
||||||
|
The operator put it in one sentence: the mesh knows the subjects, the modules do not; a module should
|
||||||
|
ask what to listen on. This record decides exactly that.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep the convention, keep it in two places.** Cheap until the next change; every change is two
|
||||||
|
changes, and the mesh cannot vary a subject for one assignment without a rule for all.
|
||||||
|
2. **Keep the convention in one place by putting it in the SDK alone**, and have the controller call
|
||||||
|
the SDK's rule. The controller is Go and the SDK is TypeScript; one of them would still carry a copy.
|
||||||
|
3. **The mesh issues the subjects.** For every assignment the controller composes a membership: what
|
||||||
|
this instance serves, where, in which queue if any; the seat verbs it holds; where its events land;
|
||||||
|
what it may reach and at which subjects. It publishes it to a subject only that assignment may read,
|
||||||
|
kept last-per-subject so a runtime that connects late reads the current one. The runtime serves
|
||||||
|
exactly the list and nothing it did not receive. The grant is composed from the same membership, in
|
||||||
|
the same act, so the two cannot drift.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Option 3.**
|
||||||
|
|
||||||
|
- **A membership per assignment.** The controller composes, for a module on a machine, one document:
|
||||||
|
the tools the module serves with the subject each is served on and the queue group if any; the seat
|
||||||
|
verbs this instance serves and their subjects; the subject each of its events lands on; what it may
|
||||||
|
reach — the tools it invokes, resolved to the subjects the mesh issued to those modules' instances —
|
||||||
|
and what it consumes. The runtime registers tools and events by name; the membership says where.
|
||||||
|
- **Published, not written into the definition.** The membership is a message on
|
||||||
|
`mesh.assignment.<node>.<module>` in a stream that keeps the last per subject, like a node's
|
||||||
|
declaration. The controller publishes it whenever the assignment's facts change: a push, a seat
|
||||||
|
handover, an instance added elsewhere, an upgrade. A runtime reads the current one when it connects,
|
||||||
|
serves it, and keeps reading, so a change reaches a running instance as a re-subscription rather
|
||||||
|
than a restart.
|
||||||
|
- **One bootstrap rule, and only one.** The credential names the node and the module; the membership's
|
||||||
|
subject follows from those two names and nothing else, and the account may subscribe it. Every
|
||||||
|
other subject is data in the membership. This is the one convention the runtime keeps, the way a
|
||||||
|
resolver keeps the address of a root.
|
||||||
|
- **The grant is the membership, read the other way.** What an account may subscribe is what its
|
||||||
|
membership says it serves plus its own membership's subject; what it may publish is what its
|
||||||
|
membership says it emits and reaches. One composition yields both, so a subject the runtime serves
|
||||||
|
without a grant, or a grant for a subject nothing serves, cannot be written.
|
||||||
|
- **Whether an instance answers for the module, or only for its machine, is the mesh's to decide.**
|
||||||
|
A module on one machine is issued the module's plain subject and its machine's. A module on several
|
||||||
|
is issued only its machine's unless its definition says its instances are interchangeable, a fact
|
||||||
|
about the software and not about the bus; then every instance is issued the plain subject in one
|
||||||
|
queue group as well. The console lists what the memberships say: a stateful module on two machines
|
||||||
|
appears once per machine; a stateless one appears once.
|
||||||
|
- **A caller composes nothing.** The console's listing carries each tool's subject; the SDK's call by
|
||||||
|
name reads the subject from the caller's own membership, where the mesh wrote what it may reach. The
|
||||||
|
shape of a subject is the controller's business and may change without any module or runtime
|
||||||
|
changing.
|
||||||
|
- **Today's shape is the shape issued first.** `mesh.mod.<module>.tool.<name>`, with the machine as the
|
||||||
|
last token for an instance, and `mesh.seat.<seat>.tool.<verb>` with the machine for a node-scoped
|
||||||
|
seat, are what the controller composes on day one, so nothing on the mesh moves when the
|
||||||
|
membership arrives; only who decides it moves. [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
|
||||||
|
stands for what it decided — a call names the machine, every answer names it, a holder serves its
|
||||||
|
seat — and is extended in how: those facts are now issued, not derived.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The runtime loses its subject rule and its claims rule; it serves a list. The controller gains one
|
||||||
|
composition and one stream; design 25 §2 and §3 gain a line each. The console loses `toolSubject`
|
||||||
|
and reads subjects from the listing. The SDK's `invokeTool` reads the caller's membership.
|
||||||
|
- A subject scheme change is a controller release and a republish of every membership, with no module
|
||||||
|
rebuilt — the opposite of this morning's forty-three builds.
|
||||||
|
- A membership can differ per assignment on purpose: an instance that holds a seat serves more; an
|
||||||
|
instance the mesh wants quiet serves less; a module the mesh is retiring can be issued nothing and
|
||||||
|
told so.
|
||||||
|
- During the move, a runtime that finds no membership for its assignment falls back to the derived
|
||||||
|
shape and says so in its log, so the wave of this change is a controller release followed by one
|
||||||
|
push, and a runtime older than the change keeps working on the convention it carries.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A membership composed for an assignment and the grant composed for its account name the same subjects, both ways | a controller test over a module on one machine, on two, holding a seat, and declared interchangeable |
|
||||||
|
| A runtime serves exactly the subjects its membership lists, and re-subscribes when the membership changes | a runtime test against a real bus: a membership published, served; republished with a subject removed and one added, followed |
|
||||||
|
| A runtime with no membership says so and serves the derived shape | the same test, before any membership is published |
|
||||||
|
| The console lists a stateful module on two machines once per machine, and composes no subject | the MCP conformance test |
|
||||||
|
| Live: the store's databases asked of one named machine and through the seat, after a controller release and one push, with no module rebuilt | by hand |
|
||||||
|
|
||||||
|
## Built, 2026-10-01
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-01.** The decision stands; these are the facts of its building.
|
||||||
|
|
||||||
|
- The controller's half: mesh-controller 188 — the membership, its subject, the assignments stream
|
||||||
|
read directly, a module's account granted its own membership and nothing else of the stream, a
|
||||||
|
membership published after each push.
|
||||||
|
- The runtime's half: mesh-tools 23 — the one address derived, the membership read and followed,
|
||||||
|
exactly the issued subjects served and re-served, the derived shape with a log line until one is
|
||||||
|
issued, a seat's verbs implemented under the seat's name and never listed as the module's, the
|
||||||
|
listing carrying subjects and the console composing none. A claim may now name the verbs it
|
||||||
|
serves for its seat (mesh-controller 186), so a holder's own tools need not be the seat's.
|
||||||
|
- What the first roll-out taught: the controller's own grant did not name the assignments it issues,
|
||||||
|
so the first memberships were refused by the server and every runtime kept the derived shape —
|
||||||
|
which is exactly the fallback this record asked for, and exactly why nobody noticed
|
||||||
|
([issue 183](../04-ISSUES/183-the-controller-could-not-publish-the-memberships-it-issued/00-report.md)).
|
||||||
|
The SDK's `invokeTool` still composes a subject; it reaches a membership through the runtime's
|
||||||
|
broker, which does, so the caller-side rule is met there and not yet in the SDK's own words.
|
||||||
|
- Live, 14:55Z the same day, through the console: the console's runtime logged *was issued a new
|
||||||
|
membership; re-serving on it*; `mesh-store.databases` answered by the control node, the seat's
|
||||||
|
holder; `postgres.postgres_list_databases` with the machine named answered by that machine, on
|
||||||
|
both machines that run it; `mesh-controller.push {node}` reached the seat's verb with its own
|
||||||
|
argument intact. Three facts the proof taught: a runtime's first read of the stream must use the
|
||||||
|
subject-addressed direct get, the only form its account is granted (mesh-tools 25); a module's
|
||||||
|
bus credential is a minted secret written once, so a claim added to a definition reaches a running
|
||||||
|
module only after `module issue <module> --node <machine>` and a push (postgres, both machines);
|
||||||
|
and a registration under a seat the credential does not yet claim must be said and skipped, not
|
||||||
|
fatal (mesh-tools 26).
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md) — extended: the same facts, issued rather than derived
|
||||||
|
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — the surface and the seat's tools this applies to
|
||||||
|
- [Design 25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §2, §3; [Design 32 — What a module declares](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §1; [Design 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md); [Design 34](../03-DESIGN/01-to-be/34-the-console.md)
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-01
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 161. What deserves a seat: a role of a module is a seat, a singular fact about machines is a placement with a capacity of one, and a holder's software is the machine's
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Three issues asked the same question from three sides. The vault provides `secret` to the whole
|
||||||
|
mesh and claims no seat, so nothing refuses a second vault by name
|
||||||
|
([issue 106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md)). The hub of the private
|
||||||
|
network is a placement, `overlay place <node> --hub`, and the issue asked whether "there is exactly
|
||||||
|
one hub" is a seat's shape ([issue 105](../04-ISSUES/105-the-hub-of-the-private-network-is-not-a-seat/00-report.md)).
|
||||||
|
Three modules claim the one uplink seat, one per network manager a machine might run, and nothing
|
||||||
|
checks that the holder names the manager the machine actually runs
|
||||||
|
([issue 138](../04-ISSUES/138-two-modules-claim-one-seat-and-are-not-interchangeable/00-report.md)).
|
||||||
|
|
||||||
|
Read against the code on the day of deciding:
|
||||||
|
|
||||||
|
- The mesh's own seats are five by [design 26](../03-DESIGN/01-to-be/26-the-seats.md)'s table and
|
||||||
|
four in the controller's seed: `mesh-vault` is in the table and not in the seed, and the vault's
|
||||||
|
definition claims nothing. The design also says `secret` is reserved; no parser or resolution rule
|
||||||
|
reserves it. A second provider of `secret` would be a second candidate, settled by a pin.
|
||||||
|
- The store already keeps one hub: a unique index since the overlay's first migration, and the
|
||||||
|
placing command refuses a second hub naming the first. What 105 observed as silent is not.
|
||||||
|
[ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) decided that
|
||||||
|
the private network becomes a mesh-scoped seat held by a server module, with client modules —
|
||||||
|
the overlay is the host's own today, so that seat has nothing to be held by yet.
|
||||||
|
- A machine's capabilities are its profile, detected by the host at enrolment and never since, and
|
||||||
|
resolution refuses a module on a machine lacking one it declares, naming the capability. The uplink
|
||||||
|
holders declare `package-manager` and `service-manager`, which every machine has.
|
||||||
|
|
||||||
|
[ADR 0126](0126-a-module-declares-its-own-seats.md) gave the reason the mesh's own seats exist:
|
||||||
|
**the mesh's own code looks them up by name.** `mesh-store` is an identifier the controller
|
||||||
|
dereferences, not a convention. That reason decides the first question; the other two are decided
|
||||||
|
by what a seat is — a role held by a module assignment — and by what the mesh can check.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. A provision the mesh itself dereferences is delivered by a mesh seat its provider claims.**
|
||||||
|
The vault's `secret` is one: the controller seals every minted credential with it. `mesh-vault` is
|
||||||
|
the fifth seat of the mesh's own, mesh-scoped, delivering `secret`, under
|
||||||
|
[ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md)'s convention; the vault's
|
||||||
|
definition claims it; a second provider of `secret` is a second claimant and refused by name. The
|
||||||
|
word *reserved* leaves design 26: the effect it described is the seat's. Every other mesh-scoped
|
||||||
|
provision — `smtp`, `oidc-client`, `s3-bucket`, `route`, `acme-ca` and the rest — may have several
|
||||||
|
providers, and a consumer with several and none local is a person's choice, as the glossary says.
|
||||||
|
The test for "deserves a seat" is the question 0126 asked: does the mesh's own code find it by name?
|
||||||
|
|
||||||
|
**2. A singular fact about machines is a placement with a capacity of one; a singular role of a
|
||||||
|
module is a seat.** A seat is held by a module assignment and points at it; the hub is a machine,
|
||||||
|
and the private network is the host's own until 0121's server and client modules exist. So the hub
|
||||||
|
stays a placement, and what a seat would have given — refusal of a second by name, and the one
|
||||||
|
named when asked — a placement of capacity one gives: the store keeps one (the unique index), the
|
||||||
|
placing command refuses a second naming the one that stands, and the overlay listing names it.
|
||||||
|
0121's seat for the private network stands, deferred with the split it needs. The rule generalises:
|
||||||
|
a fact of the shape *exactly one machine is X* is a placement checked by the store and said by name,
|
||||||
|
never a seat with no module to hold it.
|
||||||
|
|
||||||
|
**3. A holder of a seat whose role is "speak to what this machine runs" must be the dialect the
|
||||||
|
machine runs, and the machine says which.** The host's profile gains one capability per network
|
||||||
|
manager found active — `uplink-networkmanager`, `uplink-systemd-networkd`, `uplink-dhcpcd`, each
|
||||||
|
`systemctl is-active` of the manager's unit — and each uplink holder declares its own. Assignment
|
||||||
|
then refuses the wrong holder with the refusal that already exists, naming the capability; nothing
|
||||||
|
new is judged. The profile is detected again by every apply and travels in the report, and the
|
||||||
|
controller keeps the latest, so a machine that switches managers is, at its next push, a machine
|
||||||
|
whose holder lacks a capability: the plan refuses and names it, which is the one thing the machine
|
||||||
|
is the only one to know. `node-uplink` stays one seat: its three holders are three dialects of one
|
||||||
|
role, and the capability picks the dialect. One module speaking all three is allowed by this and
|
||||||
|
built by nobody.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The controller's seed gains `mesh-vault`; the seat table takes it additively at the next start,
|
||||||
|
as every seed row does. The vault's definition claims it, one release after the controller.
|
||||||
|
- The uplink definitions declare their capability one release after the host reports it, or they
|
||||||
|
are refused on every machine in between; the order is controller (the report carries a profile),
|
||||||
|
host, then catalogue.
|
||||||
|
- Design 26 loses the word *reserved* for `secret` and states rules 2 and 3; the uplink row of the
|
||||||
|
seat table names the capability its holders declare.
|
||||||
|
- Issue 106 is resolved by rule 1, 105 by rule 2 with nothing to build, 138 by rule 3.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| `mesh-vault` is in the mesh's own set, mesh-scoped, delivering `secret`, and the vault claims it | a catalogue test on the default seats; registration refuses a second claimant by name (`CanHold`'s existing test, with the vault's seat) |
|
||||||
|
| A second hub is refused naming the first, and the listing names the hub | the overlay command's test; the store's unique index |
|
||||||
|
| A machine's profile names the network manager it runs, and is renewed by every report | a host detector test per manager; a controller test that a report carrying a profile replaces the stored one |
|
||||||
|
| An uplink holder on a machine running another manager is refused, naming the capability | the existing capability refusal, exercised by a resolution test with a networkmanager machine and the systemd-networkd holder |
|
||||||
|
| Live | `mesh-controller.seats` lists `mesh-vault` held by the vault on the control node; `plan` of a machine refuses the wrong uplink holder naming `uplink-<manager>` |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md), [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md), [ADR 0117](0117-a-machines-uplink-is-a-seat.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0126](0126-a-module-declares-its-own-seats.md)
|
||||||
|
- [Design 26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md)
|
||||||
|
- Issues [105](../04-ISSUES/105-the-hub-of-the-private-network-is-not-a-seat/00-report.md), [106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md), [138](../04-ISSUES/138-two-modules-claim-one-seat-and-are-not-interchangeable/00-report.md)
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-01
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 162. A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
A merge on the forge reaches the controller as an event, and the controller asks the build
|
||||||
|
machine for what that merge changed. Until today that meant the modules whose recorded source is
|
||||||
|
that repository; since this afternoon it also means everything standing on what moved
|
||||||
|
([issue 186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md)).
|
||||||
|
Both are done inside the handler that received the event: it asks one build, waits for it, asks the
|
||||||
|
next, and returns when the last is done. Three things followed from that shape on 2026-10-01:
|
||||||
|
|
||||||
|
- The controller hears nothing else for the length of the work — twenty-five minutes for the runtime
|
||||||
|
image and its forty-three dependents ([issue 184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md)).
|
||||||
|
- A controller replaced mid-merge loses the rest of the merge: the redelivered announcement reads as
|
||||||
|
history, and the dependents are asked by hand.
|
||||||
|
- Nothing is deployed between builds. A merge that changes the build machine and something the build
|
||||||
|
machine builds asks for both in order, but the second is built by whichever build machine is running
|
||||||
|
— the old one, unless somebody pushed in between. The order the dependents are sorted in exists for
|
||||||
|
the artifacts; it says nothing about what must be *running*.
|
||||||
|
|
||||||
|
And the knowledge the order is computed from is scattered: a manifest's `build.on`, the artifacts a
|
||||||
|
build was made against, the repositories a build read, and the fact that every source-built module is
|
||||||
|
built by the build machine, each read by a different function in the merge handler.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. A module's dependencies are one relation in the catalogue.** `depends-on` edges, each with the
|
||||||
|
kind of dependency on it: `stands-on` (the module's artifact is built on the other's), `packages` (the
|
||||||
|
module's build reads the other's repository), `built-by` (the module is built by the holder of the
|
||||||
|
build-machine seat), and `declared` (a manifest's `build.on`). The relation is answered by one query
|
||||||
|
of the catalogue — the controller's inventory today, the catalogue seat's tool when something outside
|
||||||
|
the controller needs it — and nothing else computes an edge. The edges are derived from facts
|
||||||
|
recorded at two moments and written by nobody: registration records the manifest (`declared`, and
|
||||||
|
`built-by` for anything with a source), a build's take-in records what the image was built on and
|
||||||
|
which repositories it read (`stands-on`, `packages`). A module's first build places it by its
|
||||||
|
declared edges alone; from its second it is placed by what was true.
|
||||||
|
|
||||||
|
**The kinds are three dependencies, not one.** A *code* dependency — B packages A's source — means
|
||||||
|
B is rebuilt whenever A changes, in the same tier: B's build needs nothing of A's first. A *build*
|
||||||
|
dependency — B stands on A's artifact, or declares it — means B is rebuilt after A is *built*, the
|
||||||
|
next tier, and nothing need be deployed in between. A *runtime* dependency — B is built by A — means
|
||||||
|
B is rebuilt only after A is built *and running*, the next tier with a gate on the machines' reports.
|
||||||
|
One cycle is real and resolved by the kinds themselves: the runtime image is built by the build
|
||||||
|
machine, and the build machine stands on the runtime image; the image comes first, built by the
|
||||||
|
build machine that is running, which is the only one there could be — a `built-by` edge never orders
|
||||||
|
a module after a build machine that stands on it. A provision is not a dependency of this relation:
|
||||||
|
a consumer binds to its provider through what the push renders, and a change to the provider's image
|
||||||
|
changes nothing in the consumer's; a consumer whose build does read a provider's source declares it.
|
||||||
|
"A was deployed, so restart B" is the push's domain — B is replaced when what it reads changed — and
|
||||||
|
not the plan's.
|
||||||
|
|
||||||
|
**2. A merge produces a plan, and the plan is a record.** The controller takes the modules the merge
|
||||||
|
changed and everything reachable from them along `depends-on` edges, and sorts that set into tiers:
|
||||||
|
tier 0 depends on nothing else in the set, tier 1 only on tier 0, and so on. The plan — the merge it
|
||||||
|
answers, the tiers, and each module's state — is written to the store before any build is asked. The
|
||||||
|
handler asks tier 0 and returns. Every build's outcome, taken in by the same handler that takes every
|
||||||
|
outcome in, advances the plan it belongs to; a controller replaced mid-plan resumes it from the store.
|
||||||
|
|
||||||
|
**3. A tier is done when it is built, and when what the next tier needs from it is running.** A
|
||||||
|
module whose roll-out policy says *roll out* is sent to its machines when it moves, as today. The next
|
||||||
|
tier is asked only once every module in this tier is built and every rolled-out module of this tier
|
||||||
|
that a later tier is `built-by` has been applied by the machines running it — the machines' reports
|
||||||
|
say so. A module whose policy says *record* is built and not waited for. So a merge
|
||||||
|
touching the build machine and the controller builds the build machine, waits until it is the build
|
||||||
|
machine that is running, and only then asks for the controller's build.
|
||||||
|
|
||||||
|
**4. A plan is read where the mesh is read.** `status` lists every open plan: the merge, the tier it
|
||||||
|
is at of how many, what it is waiting for and since when; `builds` lists the asked beside the built.
|
||||||
|
A plan that has waited past a bound is named red there, which is the first fact of
|
||||||
|
[issue 187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md)'s list.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The merge handler returns in milliseconds; the receive loop is never held by a build again. Issue
|
||||||
|
184's remaining cause — a handler that waits for its own work — is removed rather than worked
|
||||||
|
around; the bus's heartbeats stop being dropped under a merge.
|
||||||
|
- A controller roll in the middle of a plan costs nothing: the plan is in the store and the asks are
|
||||||
|
in the queue (mesh-controller 194).
|
||||||
|
- A release across repositories is a plan whose edges cross repositories; the order a person kept in
|
||||||
|
a work-order file is the order the tiers give. Issue 186's third fault is answered by the plan,
|
||||||
|
not by a separate release record.
|
||||||
|
- The explicit `build --on <base>` stays as the way to ask for the same plan by hand.
|
||||||
|
- A module's `build.on` remains the one place a manifest states a dependency the store cannot see.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| Dependencies are one relation, each edge with its kind | an inventory test over a fixture catalogue: a runtime image, a module on it, a module packaging the controller's source, and the build machine; the four kinds come back from one call |
|
||||||
|
| A merge's set is sorted into tiers along the three kinds: a code dependency in the same tier, a build dependency after its base is built, a runtime dependency after the build machine; the build machine's own base first | a unit test on the tiering over the mesh's real shape: the runtime image, the build machine on it, modules built by it, a plugin declared on one, the proxy packaging the controller, an unrelated module left out; a cycle is one last tier and said |
|
||||||
|
| Only a runtime dependency gates on deployment, and only for a module that rolls out | the same test's gate cases |
|
||||||
|
| The plan is written before any build is asked, and the handler returns | a controller test: a merge announcement produces a plan row with its tiers and one asked build per tier-0 module, and the handler is back before any outcome |
|
||||||
|
| An outcome advances its plan; a complete tier asks the next; a tier with a rolled-out base waits for the machines' reports | store-backed tests over a two-tier plan: the first outcome marks built; the tier's roll-out gate holds until the report; the next tier is asked after |
|
||||||
|
| A controller restarted mid-plan resumes it | a test that opens a plan, drops the handler, and advances from the store alone |
|
||||||
|
| `status` lists open plans and names one that waits past the bound | the status JSON test with a fixture plan |
|
||||||
|
| Live | a catalogue merge touching a base and a dependent: the plan's tiers in `status`, the base rolled before the dependent is asked |
|
||||||
|
|
||||||
|
## Built and proven live, 2026-10-01
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-01.** The decision stands; these are the facts of its building.
|
||||||
|
|
||||||
|
Built in mesh-controller 197 (the relation, the plan record, the driver, `status`), 198 (`plans`),
|
||||||
|
199 (a `built-by` edge orders and gates but never widens — the first live plan had taken the whole
|
||||||
|
catalogue along for a controller change; `plans stop`), 200. The first merge handled by the finished
|
||||||
|
machinery, at 18:56Z, was a controller change and produced the plan this record describes: tier 0
|
||||||
|
the build machine; tier 1 the controller and the proxy that packages its source. The handler
|
||||||
|
returned at once; the build machine was built, rolled, and the plan read *tier 0 built; waiting for
|
||||||
|
builder on novox to be applied* until the machine reported; then tier 1 was asked, both built, and
|
||||||
|
the plan read done — three minutes, read through the console with `plans`, the receive loop taking
|
||||||
|
reports throughout. What the day between decision and proof taught is in issues
|
||||||
|
[184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md),
|
||||||
|
[186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md) and
|
||||||
|
[188](../04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md).
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0157](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
|
||||||
|
- [Design 30 — The mesh updates itself on a push](../03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md)
|
||||||
|
- Issues [184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md), [186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md), [187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md)
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-01
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 163. Taking a module over is a comparison: what it compares, what it refuses, and what it carries
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
On an adopted machine the mesh holds what it finds until the module is taken, and taking is the
|
||||||
|
cutover ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)). The whole-node flip
|
||||||
|
is previewed and confirmed by digest; the per-module cutover, the step that actually replaces a
|
||||||
|
running service, previews nothing. `take` names the held things the next push will replace and
|
||||||
|
where each original is kept. It does not say how the module's version of each differs from what
|
||||||
|
runs. Ten issues from the first migrations are the same omission seen from ten sides:
|
||||||
|
|
||||||
|
- a port narrowed from everywhere to the private network, unannounced ([086](../04-ISSUES/086-taking-a-module-narrows-a-port-without-saying-so/00-report.md));
|
||||||
|
- a configuration file replaced whole, dropping the one line that was the installation's own ([098](../04-ISSUES/098-taking-a-module-replaces-a-configuration-nobody-compared/00-report.md));
|
||||||
|
- an image pin that had aged into a downgrade, discovered by three minutes of outage ([099](../04-ISSUES/099-a-modules-image-pin-ages-into-a-downgrade/00-report.md));
|
||||||
|
- a secret minted for a service that already had one, with no way to carry the existing value in because it was a required secret and not the module's own ([100](../04-ISSUES/100-a-minted-secret-cannot-be-the-one-the-service-already-uses/00-report.md));
|
||||||
|
- a container moved onto the module's own network, out of reach of the neighbour that called it by name ([101](../04-ISSUES/101-taking-a-service-reached-by-container-name-cuts-its-neighbours-off/00-report.md));
|
||||||
|
- a resource whose target changed, leaving the old container running with no record naming it ([097](../04-ISSUES/097-a-resource-that-changes-target-leaves-the-old-one-behind/00-report.md));
|
||||||
|
- a volume path that changed without the running container noticing, because the host does not compare that field ([126](../04-ISSUES/126-a-volume-path-is-not-in-the-spec-comparison/00-report.md));
|
||||||
|
- a build that deployed at once because the module's policy said so, racing a data move ([126](../04-ISSUES/126-a-volume-path-is-not-in-the-spec-comparison/00-report.md));
|
||||||
|
- a setting accepted where it was set and refusing the whole machine where it was read ([096](../04-ISSUES/096-a-setting-that-cannot-work-is-stored-and-stops-the-node/00-report.md));
|
||||||
|
- a module that could not take over what genesis raised, because the two differed in name, network, data and image ([090](../04-ISSUES/090-the-forge-module-does-not-take-over-the-forge-genesis-raised/00-report.md));
|
||||||
|
- a successor that could not stand beside its predecessor at all, answered by [ADR 0104](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md)'s adapter ([093](../04-ISSUES/093-the-successor-proxy-cannot-serve-what-the-predecessor-still-serves/00-report.md)).
|
||||||
|
|
||||||
|
What the host records of a found thing is enough to compare from: a file's original, kept, with
|
||||||
|
its digest, mode and owner; a container's id and whether it ran; whether anything changed it
|
||||||
|
since. What it does not yet record is what a comparison needs most: the found container's image
|
||||||
|
and when that image was made, the networks it is on and who else is on them, what it mounts, what
|
||||||
|
it publishes. And the controller's rule that a machine is told everything or nothing turns one
|
||||||
|
impossible statement into a machine nobody can talk to.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. A take is previewed, and the preview is a comparison.** For every held thing the module would
|
||||||
|
replace, `take` puts what runs beside what the module declares and says the difference:
|
||||||
|
|
||||||
|
- a **container**: its image against the module's, with each image's creation date so older and
|
||||||
|
newer have a meaning; its name; its networks, and the other containers on each found network
|
||||||
|
that is not the module's; its published ports and the reach of each, found firewall and guard
|
||||||
|
included; its mounts against the module's volumes and paths;
|
||||||
|
- a **file**: the kept original against the declared content, as a difference, not two digests;
|
||||||
|
- a **secret** the module takes that the mesh minted and nobody accepted, when the service's data
|
||||||
|
was found — a service that already runs already has a value;
|
||||||
|
- the module's **settings** on that machine, composed against its definition.
|
||||||
|
|
||||||
|
`take` without `--yes` prints the comparison and stops; `take --yes <digest>` cuts over exactly
|
||||||
|
what was previewed, the way the flip is confirmed, and a preview whose account of the machine is
|
||||||
|
older than the flip allows is refused the same way. The host supplies the facts in its report of
|
||||||
|
what it holds: the found container's image and its creation date, its networks and their members,
|
||||||
|
its mounts and published ports.
|
||||||
|
|
||||||
|
**2. Three differences refuse by default, each overridden by naming it.** An image **older** than
|
||||||
|
the one running, by creation date — `--downgrade`, said once and recorded. A declared file that
|
||||||
|
**differs** from the kept original — `--replace <path>`, or the module declares the file partially
|
||||||
|
and writes into it ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)), which is
|
||||||
|
the right answer wherever the file is the service's own and the format allows it. A **minted,
|
||||||
|
unaccepted secret** for a service whose data was found — accept the value first, or `--mint
|
||||||
|
<name>` to say the service shall take a new one. Two differences are said and not refused: a port
|
||||||
|
whose reach **narrows**, and a found network whose other members may reach the container **by
|
||||||
|
name**, each member named; both are the operator's to weigh, and the words are there to weigh them.
|
||||||
|
|
||||||
|
**3. A secret the mesh would mint may be accepted instead, own or required.** `secret accept`
|
||||||
|
reaches a module's required secrets, not only its own: the value is a fact about the machine, and
|
||||||
|
the mesh's job at a take is to learn it. The accepted value is sealed to the module as a minted one
|
||||||
|
would be, and the provider that would have minted it is told it has one. Whether one accepted
|
||||||
|
value should reach every consumer of a provider at once is [issue 165](../04-ISSUES/165-one-accepted-value-must-be-accepted-once-per-consumer/00-report.md)'s
|
||||||
|
question and the next group's.
|
||||||
|
|
||||||
|
**4. A taken container may keep a found network, for a while, by a setting.** A per-machine
|
||||||
|
setting names a found network the module's container also joins, so a neighbour that resolves it
|
||||||
|
by name keeps resolving it. It is migration scaffolding in the sense of
|
||||||
|
[ADR 0104](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md): assigned only on an
|
||||||
|
adopted machine, reported while it stands, removed when the neighbours are taken, and the preview
|
||||||
|
names it. Taking a group of modules at once is not decided here; the setting makes the order free.
|
||||||
|
|
||||||
|
**5. The host compares every field it writes, and removes what it can no longer name.** A
|
||||||
|
container is current when every field the host would write agrees with the one running — volumes
|
||||||
|
and paths included; a field the host cannot compare recreates rather than passes. The host's
|
||||||
|
record keeps a resource's former targets: a container or file the host **wrote** under a name or
|
||||||
|
path the declaration no longer names is removed on the next apply and said; what was **found** is
|
||||||
|
never removed, as ADR 0100 says. And the host answers the question nothing answered on
|
||||||
|
2026-09-23: its report lists what runs on the machine that the mesh neither wrote nor holds —
|
||||||
|
containers and listeners — as *strays*, so a thing left behind is seen the day it is left.
|
||||||
|
|
||||||
|
**6. A setting is judged where it is stored, and an impossible one costs a module, not a machine.**
|
||||||
|
Storing a setting composes it against the module's current definition and refuses with the node,
|
||||||
|
module, layer and key when it cannot work. A definition that later moves under a stored setting
|
||||||
|
makes composition leave *that module* out of the machine's declaration — its held things kept, its
|
||||||
|
containers untouched — and say the statement by name; the machine is still told everything else.
|
||||||
|
A machine is told everything or nothing about what it *is* told; what it is not told is said.
|
||||||
|
|
||||||
|
**7. What genesis raises, it raises as the module that succeeds it declares** — name, network,
|
||||||
|
data directory and image — so the module adopts it by the found rule that already exists, and a
|
||||||
|
module meant to succeed a bootstrap service that it cannot adopt is a fault of genesis, found by a
|
||||||
|
test that raises and then assigns. **`build` says when a policy will act on its result**, so a
|
||||||
|
person choreographing a data move knows which module will not wait; under
|
||||||
|
[ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md) the roll-out is the plan's, and
|
||||||
|
the plan says it too.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- `take` becomes the per-module twin of the flip: preview, digest, confirm. The flip's own preview
|
||||||
|
gains the same comparisons for every module it takes.
|
||||||
|
- The host's report of what it holds grows by the found container's image and creation date,
|
||||||
|
networks and members, mounts and published ports; its store keeps former targets and strays.
|
||||||
|
- Issues 086, 098, 099, 100, 101 close on rule 1 and 2; 097 and 126 on rule 5; 096 on rule 6;
|
||||||
|
090 on rule 7; 093 is closed by ADR 0104's adapter, which runs.
|
||||||
|
- Nothing here changes what an adopted machine keeps or when: found stays held, held is never
|
||||||
|
removed, the original is kept before anything is written.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| The host reports a found container's image and creation date, networks and their members, mounts and published ports | host unit tests over a fake runtime; the adoption bed's report |
|
||||||
|
| `take` without `--yes` previews every held thing's difference and changes nothing; `--yes` with the digest cuts over; a stale account is refused | controller tests over a fixture report: a differing file, an older image, a narrowed port, a shared network, a minted secret |
|
||||||
|
| An older image, a differing file and a minted secret for found data refuse without their override | the same tests |
|
||||||
|
| A found network kept by a setting is joined, reported and named in the preview | a host test and a controller resolution test |
|
||||||
|
| `secret accept` takes a required secret | an inventory test; the provider is told |
|
||||||
|
| Every container field is compared; a former target the host wrote is removed and said; what was found is not | host tests: a volume path change recreates; a renamed container's predecessor is removed; a found one under the old name is kept |
|
||||||
|
| Strays are reported | a host test over a fake runtime with a container nobody declared |
|
||||||
|
| A setting that cannot compose is refused where stored, naming node, module, layer, key; a definition moving under one leaves that module out and says so | controller tests |
|
||||||
|
| Genesis raises the forge as its module declares it | a genesis test that raises, assigns, and finds the module holding rather than raising a second |
|
||||||
|
| Live | the next cutover on an adopted machine: `take` shows the comparison, refuses the downgrade if there is one, and the service keeps its configuration and its secret |
|
||||||
|
|
||||||
|
## Built, 2026-10-02
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-02.** The decision stands; these are the facts of its building.
|
||||||
|
|
||||||
|
Built across mesh-host 63 and 64 and mesh-controller 201, 202 and the pull request that followed
|
||||||
|
them. Rule 1: `take` previews every held thing's comparison and ends with a digest; `take --yes
|
||||||
|
<digest>` acts on exactly that preview, and a changed preview or an account older than the flip
|
||||||
|
allows is refused, as the flip's are. A published port's reach is said as the machine reported it,
|
||||||
|
behind the found firewall whose rules are not read. Rule 2: an older image, a differing file and a
|
||||||
|
minted, unaccepted secret for found data refuse, overridden by `--downgrade`, `--replace <path>` and
|
||||||
|
`--mint <name>`; the secrets a module holds on a machine are read with where each came from. Rule 3:
|
||||||
|
`secret accept --provider` reaches a required secret. Rule 4: the per-machine setting is `networks`,
|
||||||
|
a container id to the found networks it keeps; judged for an adopted machine only, joined by the host
|
||||||
|
after the container runs, part of the container's spec, named in the preview. Rule 5: the host's
|
||||||
|
facts, former targets and strays. Rule 6: one judgement, run where a setting is stored and where a
|
||||||
|
machine is composed; a module whose stored setting its definition can no longer compose is left out
|
||||||
|
of the declaration, the envelope says so, the host keeps that module's things, and `plan` and `push`
|
||||||
|
say it by name. A key that reaches nothing is refused where stored and said by `plan`, and never
|
||||||
|
costs a module. Rule 7: genesis raises the forge under the module's container name, with its image
|
||||||
|
digest and its data directory; the network is the one difference left, said by the take, because the
|
||||||
|
bootstrap forge reaches the store on the machine's loopback.
|
||||||
|
|
||||||
|
**Not yet proven live.** Every machine of this mesh is converged, so the table's last row — a take
|
||||||
|
on an adopted machine — waits for the next adoption. What is live is what the rows above it check.
|
||||||
|
Issues 086, 098, 099, 100 and 101 stay located until that row is read.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), [ADR 0103](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md), [ADR 0104](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md), [ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md)
|
||||||
|
- [Design 05 — The node host](../03-DESIGN/01-to-be/05-the-node-host.md), [Design 09 — The node lifecycle](../03-DESIGN/01-to-be/09-the-node-lifecycle.md)
|
||||||
|
- Issues 086, 090, 093, 096, 097, 098, 099, 100, 101, 126
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 167. A membership carries what its module receives, and who the mesh is
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
A provider learns what it is given from a file. The controller composes every consumer's contribution
|
||||||
|
to a requirement, and the node's declaration writes them into the provider's received file. The route
|
||||||
|
proxy reads its routes that way: one JSON file, re-read every two seconds.
|
||||||
|
|
||||||
|
[Issue 191](../04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/00-report.md) showed what
|
||||||
|
that file leaves out. Since [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md),
|
||||||
|
a route whose endpoint reaches only the private network carries an internal name and no public one.
|
||||||
|
The proxy dropped it. Serving it was not enough either: the proxy answers public and internal names on
|
||||||
|
the same listeners, so an internal name served to every request is public under a guessable name. To
|
||||||
|
serve it correctly the proxy needs a second fact, **who the mesh is**, and nothing gave it one.
|
||||||
|
|
||||||
|
The first attempt had the proxy work it out: the mesh's range from an environment variable written by
|
||||||
|
the catalogue, and the machine's container bridges read from its own interfaces. That is a second
|
||||||
|
definition of "the mesh", kept by one module, beside the one the packet filter already uses. The
|
||||||
|
controller resolves "from the mesh" to every machine's address on the private network, and the filter
|
||||||
|
is rendered from that list. Two definitions agree until one changes.
|
||||||
|
|
||||||
|
[ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||||
|
already gives every assignment one document on the bus, its membership, read once at connect and
|
||||||
|
followed. It says what the assignment serves and reaches. It does not yet say what it is given.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep the file, add the mesh to it.** The proxy keeps polling a file, and the controller writes the
|
||||||
|
mesh's addresses beside the routes. It fixes the definition, but delivery stays a file re-read on a
|
||||||
|
timer, written by a separate path from the one every other fact a module is told now takes.
|
||||||
|
2. **Have the proxy work it out** from a range in its environment and the machine's interfaces. Rejected:
|
||||||
|
it is the second definition this record exists to remove.
|
||||||
|
3. **The membership carries it.** What each module receives, from the same composition its received
|
||||||
|
file is written from, and the mesh's addresses, from the same list the filter is rendered from. The
|
||||||
|
proxy follows its membership and serves exactly that.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Option 3.**
|
||||||
|
|
||||||
|
- **A membership carries what its module receives**, by requirement: the contributions every consumer
|
||||||
|
made, exactly as composed for its received file. A requirement nobody contributed to is an empty
|
||||||
|
list, never absent, for the reason the file is written empty: "nothing asked" and "never told" want
|
||||||
|
different responses.
|
||||||
|
- **A membership carries who the mesh is**: every machine's address on the private network, the list
|
||||||
|
a rule saying "from the mesh" resolves to. One list, two readers: the filter and any module that
|
||||||
|
must tell the mesh from the world.
|
||||||
|
- **The route proxy reads its routes and the mesh from its membership**, with the bus account every
|
||||||
|
module that speaks on the bus is given. It serves an internal name only to the machines the mesh
|
||||||
|
names and to the machine itself, and answers anyone else as it answers a name it never routed: in
|
||||||
|
the request, in the handshake, and in the list of names it serves.
|
||||||
|
- **The file stays until the bus has spoken.** While a proxy has read no membership that carries routes,
|
||||||
|
it serves the file, and an internal name only to its own machine: refused, never opened. A
|
||||||
|
membership from a controller that issues no routes changes nothing.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Every membership grows two fields. A machine joining or leaving republishes every membership, which
|
||||||
|
a push already does.
|
||||||
|
- A provider that receives something is told it twice for now, in its file and on the bus. The file
|
||||||
|
goes when every provider reads its membership; that is its own change.
|
||||||
|
- The route proxy needs a bus account. It is issued like any module's, so a machine running the proxy
|
||||||
|
cannot be composed between the catalogue declaring the account and the operator issuing it. The
|
||||||
|
machine keeps what it runs meanwhile.
|
||||||
|
- The internal name of a route that also has a public one is now served to the mesh only. Outsiders
|
||||||
|
have the public name.
|
||||||
|
- A container on the same machine that calls that machine's own internal name arrives from its
|
||||||
|
container network, not from a mesh address, and is refused. Calls between machines are unaffected:
|
||||||
|
they leave by the machine's mesh address. Whether the mesh should also issue each machine's container
|
||||||
|
networks is left open, because the mesh does not record them today.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| What a provider receives on the bus is what its received file says, same-node port fix included | a controller test composing a provider and a consumer on one machine and comparing the two |
|
||||||
|
| An internal name is served to the machines the membership names and to loopback, and to nobody else | the proxy's tests: served from a named address and from loopback; refused, unlisted and uncertified from any other |
|
||||||
|
| A membership that carries no routes, or a mesh that cannot be read, changes nothing | the proxy's tests |
|
||||||
|
| Until the mesh is issued, an internal name is served to the machine alone | the proxy's tests |
|
||||||
|
| Live: an internal-only route answers over the mesh and is refused from outside | by hand, after the release |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md) —
|
||||||
|
the membership this extends
|
||||||
|
- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — reach, and the
|
||||||
|
insight of 2026-10-02 that the proxy is where internal reach is kept
|
||||||
|
- [ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md) — the machine itself is always inside
|
||||||
|
- [Issue 191](../04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/00-report.md) — what
|
||||||
|
found it
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 168. A converged machine is filtered by the mesh alone, and the host says what else refuses
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) says what converging does to
|
||||||
|
the firewall a machine was found with: the mesh's derived filter is loaded in place of the
|
||||||
|
refusal-only guard, and the found firewall is retired — disabled, never flushed. Four issues from
|
||||||
|
the first two convergences are four ways that sentence was not the machine:
|
||||||
|
|
||||||
|
- the flip reported the found firewall retired and it was active two minutes later; fifty minutes
|
||||||
|
on, a reconcile found it disabled by hand and recorded that the mesh had done it
|
||||||
|
([143](../04-ISSUES/143-converging-does-not-retire-the-firewall-it-found/00-report.md));
|
||||||
|
- "the firewall found" named one front end, and what filtered the forwarded path on that machine
|
||||||
|
was a chain a predecessor had installed in the container runtime's user hook — invisible to the
|
||||||
|
mesh, refusing two ports the mesh declared open, and when it was removed, carrying an allowance
|
||||||
|
every module reaching another by the machine's own name had been relying on
|
||||||
|
([144](../04-ISSUES/144-the-predecessors-rules-outlive-the-firewall-it-was-found-as/00-report.md),
|
||||||
|
[145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md));
|
||||||
|
- the forward chain listed address ranges that followed neither the modules nor the machine
|
||||||
|
([141](../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md)), answered by
|
||||||
|
[ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md) before this record;
|
||||||
|
- the networking module wrote two machine-wide files whole, so taking it restarted every
|
||||||
|
container ([084](../04-ISSUES/084-taking-networking-on-an-adopted-node-restarts-every-container/00-report.md)),
|
||||||
|
answered by [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md) and the hosts
|
||||||
|
file's marked region ([issue 128](../04-ISSUES/128-the-hosts-file-is-written-whole/00-report.md)).
|
||||||
|
|
||||||
|
Read on the four machines of this mesh on 2026-10-02, after every one had converged: on both
|
||||||
|
machines that had a front end it is inactive, and the host's record says the mesh retired it on
|
||||||
|
both — true of one, false of the other. On the home server the predecessor's chain is still in
|
||||||
|
force on the forwarded path, in the legacy packet filter the mesh's reader of rules does not
|
||||||
|
consult once a machine is converged, so that machine is filtered by two things and the mesh says
|
||||||
|
one. The host's reader already knows how to tell a table that refuses traffic from the runtime's
|
||||||
|
own plumbing and from a ban list; it is asked once, at adoption, and only to refuse a machine
|
||||||
|
whose firewall nobody speaks. Nothing asks it afterwards, and nothing reports what it saw.
|
||||||
|
|
||||||
|
The group's exit is one sentence: *a converged machine has exactly one thing filtering it, and
|
||||||
|
the mesh says truthfully which.* The first half the mesh can enforce only for what it owns; the
|
||||||
|
second half it can always do, and it is the half that was missing.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. Convergence is a state the host keeps, not a step it takes once.** Every apply of a converged
|
||||||
|
declaration reads whether the found firewall is in force. Active — enabled again by a package, a
|
||||||
|
boot, a hand — it is retired again and said. The record distinguishes *the mesh disabled it* from
|
||||||
|
*it was found inactive*, and a reconcile that finds it inactive never records that the mesh did
|
||||||
|
it. When the step is skipped because the apply had failures, the report says the found firewall
|
||||||
|
was left in force and why; a step that does nothing is never silent.
|
||||||
|
|
||||||
|
**2. The host reports what filters the machine, with every apply, adopted or converged.** Every
|
||||||
|
table of the packet filter, and every chain of the legacy filter, that refuses traffic — a drop or
|
||||||
|
a reject, or a base chain whose policy drops — with an owner: the *mesh's*, the *found firewall's*,
|
||||||
|
the *container runtime's own*, a *ban* (a refusal that names the sources it refuses, in a chain
|
||||||
|
that accepts nothing), or *other*. The runtime's own is its plumbing — its chains, the forward
|
||||||
|
policy it sets when it turns forwarding on, its guard against reaching a container's address from
|
||||||
|
off its bridge. The user chain the runtime leaves for an administrator is not the runtime's:
|
||||||
|
anything refusing in it is *other*, which is where both predecessors' chains lived. Each entry
|
||||||
|
says in one line what it refuses. The mesh removes none of it: a rule it did not write is the
|
||||||
|
operator's to remove, now that they can see it.
|
||||||
|
|
||||||
|
**3. The mesh says which.** `node show` lists the filters with their owners. `status` names every
|
||||||
|
converged machine that something other than the mesh's table, the runtime's plumbing and a ban
|
||||||
|
list filters, the way it names strays and untaken modules, and such a machine is not "all well".
|
||||||
|
The converge preview lists the filters found and the fate of each: the found firewall retired, the
|
||||||
|
runtime's and the bans left, *other* left and named — so a person knows before the flip that the
|
||||||
|
machine will not be filtered by the mesh alone until they remove it, and what they would be
|
||||||
|
removing. *A converged machine is filtered by the mesh alone* when its list holds nothing but the
|
||||||
|
mesh's, the runtime's own and bans.
|
||||||
|
|
||||||
|
**4. Adoption's threshold does not move.** A machine whose front end nobody speaks is still refused
|
||||||
|
adoption; a refusing rule in the runtime's user chain still does not refuse it — on both machines
|
||||||
|
of this mesh it would have, and the migration would not have happened. It is reported instead,
|
||||||
|
from the first report on.
|
||||||
|
|
||||||
|
**5. Two of the group's issues are settled by records already accepted.** The forward chain follows
|
||||||
|
the machine's outward links and says nothing about networks ([ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md)),
|
||||||
|
which answers 141 whole. The runtime's file is written into and reloaded, and the hosts file's
|
||||||
|
region is the mesh's alone ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md),
|
||||||
|
[issue 128](../04-ISSUES/128-the-hosts-file-is-written-whole/00-report.md)), which answers 084. One
|
||||||
|
machine-wide file the mesh still writes whole is its own filter, at the path the distribution's
|
||||||
|
packet filter reads; an operator's own rules at that path would be contested, and are held as
|
||||||
|
found until the filter module is taken ([ADR 0163](0163-taking-a-module-over-is-a-comparison.md)).
|
||||||
|
That is a difference a take shows, not a fault, and is decided when it bites.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The host's report grows by the filters it found and, for a converged machine, the state of its
|
||||||
|
found firewall and who retired it; the controller keeps both on the node's record.
|
||||||
|
- `retireFirewall` runs on every converged apply and can disable the found firewall more than
|
||||||
|
once; the record's *disabled by the mesh* means exactly that.
|
||||||
|
- The reader of rules gains an owner per table and chain; what it refuses adoption for does not
|
||||||
|
change. A ban stays what it was: not a firewall.
|
||||||
|
- Issues 143 and 144 close on rules 1 to 3 once a machine's record names the predecessor's chain;
|
||||||
|
141 closes on ADR 0140 and 084 on ADR 0102, both by reading.
|
||||||
|
- Removing what is reported is the operator's act, by hand, with the preview's words in front of
|
||||||
|
them. The mesh never flushes and never deletes a rule it did not mark.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| Every refusing table and chain is classified, the user chain's refusals as *other* | host tests over rulesets captured from three machines of this mesh: a predecessor's chain in the legacy filter, a ban list and empty front-end chains beside the runtime's, a virtualisation host and an endpoint agent that refuse nothing |
|
||||||
|
| The found firewall active again on a converged machine is retired again and said; found inactive is recorded as found, not done; a skipped step is said | host tests over a fake front end |
|
||||||
|
| The report carries the filters and the found firewall's state for a converged machine | a host test reading the report |
|
||||||
|
| `node show` lists filters with owners; `status` names a converged machine something else filters and is not well; the preview lists filters and fates | controller tests over a fixture report |
|
||||||
|
| Live | the home server's record names the predecessor's chain in the runtime's user chain as *other*; `status` names the machine; after the operator removes the chain, the next report drops it and `status` is well |
|
||||||
|
|
||||||
|
## Built and proven live, 2026-10-02
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-02.** The decision stands; these are the facts of its building.
|
||||||
|
|
||||||
|
Built in mesh-host 67 (every refusing table and legacy chain classified with an owner, reported with
|
||||||
|
every apply; the found firewall retired on every converged apply, *found inactive* kept apart from
|
||||||
|
*disabled by the mesh*, a skipped step said) and mesh-controller 211 (kept per node, shown on `node
|
||||||
|
show`, named by `status` and not well, previewed with fates). The live row was read at 10:10Z: the home
|
||||||
|
server's record named the predecessor's chain in the legacy filter's user chain as *other*, beside two
|
||||||
|
chains a retired front end left in the IPv6 legacy filter; the control node's record named the same two
|
||||||
|
leftovers; the laptop and the workstation read *the mesh alone*; `status` named both machines. The five
|
||||||
|
rule sets were removed at 12:46Z through the packet filter seat's `remove` verb
|
||||||
|
([ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)), and the next report read *the mesh alone* on
|
||||||
|
all four machines. The control node's record still says the mesh retired its front end, which issue 143
|
||||||
|
records as a hand's work: the host trusts its record, and from this build on the distinction is kept.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [ADR 0103](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md), [ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), [ADR 0163](0163-taking-a-module-over-is-a-comparison.md)
|
||||||
|
- [Design 08 — Connectivity](../03-DESIGN/01-to-be/08-connectivity.md), [Design 05 — The node host](../03-DESIGN/01-to-be/05-the-node-host.md)
|
||||||
|
- Issues 084, 141, 143, 144, 145
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0004-a-node-and-how-it-joins.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 169. A machine joins through the tunnel, and the bus is never public
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The bus is the one channel every machine depends on: enrolment, every declaration, every tool. The
|
||||||
|
`nats` module declares it reachable from the mesh only. The controller still opens it to the whole
|
||||||
|
internet on the machine that runs it, as a *foundation* port that no module declares and nothing may
|
||||||
|
close ([issue 051](../04-ISSUES/051-the-mesh-cannot-update-what-it-depends-on/00-report.md)).
|
||||||
|
The reason is joining. [ADR 0004](0004-a-node-and-how-it-joins.md) has a new machine enrol over the bus
|
||||||
|
**before** it has a tunnel. [ADR 0007](0007-connectivity.md) states it as a requirement: the node
|
||||||
|
running the broker must be reachable from wherever nodes are, at a stable address.
|
||||||
|
|
||||||
|
So the bus listens on the internet permanently, for an event that happens a few times a year. A
|
||||||
|
sweep of every machine on 2026-10-02 found no client using the public path. Every connection arrives
|
||||||
|
over the tunnel or from the machine itself. The join token does not use it either: it carries the
|
||||||
|
controller's configured broker address, a mesh name with the old broker's port.
|
||||||
|
|
||||||
|
ADR 0004 already says what a joining machine needs: *an identity, an address, and one peer to reach*.
|
||||||
|
The tunnel can be that peer, if the hub knows the new machine's key before the machine first knocks.
|
||||||
|
WireGuard answers nothing to a key it does not know, which is why the tunnel's own port is safe to
|
||||||
|
leave open where the bus's is not.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep the bus public.** It is authenticated and encrypted, but every exposure of it, and of the
|
||||||
|
server behind it, is exposure of the one thing everything depends on.
|
||||||
|
2. **Open the bus publicly only while a join token is live.** Small, and the hub is open only during a
|
||||||
|
join window. But the window is real, the rule is about time rather than about who may reach the
|
||||||
|
bus, and the opening and closing are pushes that can fail between them.
|
||||||
|
3. **The controller makes the new machine's tunnel key and puts it in the token.** One step for the
|
||||||
|
operator, but the private half leaves a machine it does not belong to. ADR 0004 refuses that for
|
||||||
|
every key a node holds.
|
||||||
|
4. **The machine makes its key first, and the token is issued for it.** The machine prints the public
|
||||||
|
half of its tunnel key. The operator issues the token for that key. The controller gives the
|
||||||
|
machine its address and adds it as a peer on the hub. The token carries the hub's tunnel endpoint
|
||||||
|
and key, the machine's address, and the bus's address on the private network. The machine brings
|
||||||
|
up its tunnel and enrols over it.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Option 4.**
|
||||||
|
|
||||||
|
- **A machine makes its own tunnel key before it has a token**, and prints the public half. The private
|
||||||
|
half never leaves it, as ADR 0004 says of every key a node holds.
|
||||||
|
- **A token is issued for a tunnel key.** Issuing it assigns the machine's address on the private
|
||||||
|
network, records the key, and makes the machine a peer of the hub. The hub is sent that before the
|
||||||
|
token is shown, so the tunnel answers the moment the machine first uses it.
|
||||||
|
- **The token carries the one peer.** It adds the hub's tunnel endpoint and public key and the
|
||||||
|
machine's own address. **Where** becomes the bus's address on the private network, which needs no
|
||||||
|
name resolution.
|
||||||
|
- **The machine joins through the tunnel.** It brings the tunnel up from the token alone, then enrols
|
||||||
|
over it exactly as before. The enrolment checks that the key it is offered is the one the token was
|
||||||
|
issued for.
|
||||||
|
- **The bus is never public.** It is no longer a foundation port. Its reach is what the `nats` module
|
||||||
|
declares: the mesh. The tunnel's port stays open, as the one way in.
|
||||||
|
|
||||||
|
This changes three things earlier records say. ADR 0004's *where* is the bus's private address, and the
|
||||||
|
token carries the peer. ADR 0007's requirement that the broker be reachable from wherever nodes are
|
||||||
|
becomes: **the hub's tunnel is**. Issue 051's broker port stops being a foundation port.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Joining is two commands on the new machine, with the token issued between them. A token issued for
|
||||||
|
the wrong key gives a tunnel that never answers, and the machine says so rather than timing out at
|
||||||
|
the bus.
|
||||||
|
- An unused token leaves a peer on the hub until it expires. Expiry removes it, the same way it voids
|
||||||
|
the secret.
|
||||||
|
- A machine already in the mesh is unaffected: it reaches the bus over its tunnel today.
|
||||||
|
- The genesis machine, the first one, raises the bus on itself and needs no tunnel to reach it.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A token is refused without a tunnel key, and carries the hub's peer and the machine's address | a controller test |
|
||||||
|
| Issuing a token makes the machine a peer of the hub before the token is shown | a controller test over the hub's composed tunnel |
|
||||||
|
| An expired, unused token's peer is gone from the hub | a controller test |
|
||||||
|
| Enrolment refuses a tunnel key other than the one the token was issued for | a controller test |
|
||||||
|
| No machine's filter opens the bus to anywhere | a controller test over the composed filter, and the live sweep from outside the mesh |
|
||||||
|
| A new machine joins from outside the hub's network with the bus closed to it | the lab, then by hand |
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 170. The firewall seat serves its verbs, and a foreign rule set is removed through one of them
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md) made the mesh say truthfully
|
||||||
|
what filters a converged machine, and left the removal of what it did not write to the operator's
|
||||||
|
hand. The first time that hand was needed — two machines, five rule sets a predecessor and a
|
||||||
|
retired front end had left — there was no mesh way to lend it: the packet filter is a seat
|
||||||
|
([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)), a seat's
|
||||||
|
holder serves its verbs ([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md),
|
||||||
|
[ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)), and the
|
||||||
|
firewall seat declared none. The only remaining path was a shell on the machine, which is the path
|
||||||
|
the mesh exists to replace, and which the operator's own tooling rightly refused to an agent.
|
||||||
|
|
||||||
|
A seat's verbs are the contract every holder implements, whatever filter it speaks. What a person
|
||||||
|
asks a machine's packet filter is the same whether nftables, a front end or a legacy filter answers:
|
||||||
|
what are the rules, reload the mesh's own, remove this thing the mesh did not write. What differs by
|
||||||
|
filter is the holder's own business and may be its own tools beside the seat's.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. The `node-packet-filter` seat serves three verbs**, and a module that claims it serves all
|
||||||
|
three or is refused the claim, as with every seat:
|
||||||
|
|
||||||
|
- `rules` — the packet filter as the machine enforces it now: the nftables ruleset, and the legacy
|
||||||
|
filter's listings where that tool exists; narrowed to one table or chain when asked. Read-only.
|
||||||
|
- `reload` — load the mesh's own filter again from the file the mesh writes, and answer with the
|
||||||
|
mesh's table as loaded. The holder's own act on the holder's own rules.
|
||||||
|
- `remove` — remove one rule set the mesh did not write, named exactly as the host reports it under
|
||||||
|
ADR 0168 (`chain HAL-MESH-ONLY (iptables-legacy)`, `table ip6 filter, chain DOCKER-USER`), and
|
||||||
|
answer with what was done. It refuses the mesh's own tables, the container runtime's own chains,
|
||||||
|
a built-in chain other than the runtime's user chain, and any chain of a found firewall that is
|
||||||
|
in force. The runtime's user chain is emptied back to its one return; another chain loses the
|
||||||
|
jumps into it, is flushed and deleted; a table of the machine's own is deleted whole. Each is an
|
||||||
|
operator's act, by name, on one thing the mesh reported — never a flush, never a rule the mesh
|
||||||
|
itself marked.
|
||||||
|
|
||||||
|
**2. A holder may serve its own tools beside the seat's.** The nftables module keeps its reading of
|
||||||
|
the mesh's table as its own tool, and a holder speaking a filter with specifics of its own may add
|
||||||
|
tools for them; the seat's three are what every holder owes.
|
||||||
|
|
||||||
|
**3. A container may ask for a capability.** Serving `remove` and `reload` needs the machine's
|
||||||
|
network namespace and the right to change its packet filter; a holder's runtime declares
|
||||||
|
`capabilities: ["NET_ADMIN"]` on its container and runs on the machine's network. The host grants
|
||||||
|
exactly the capabilities declared, names them in the container's spec so a change recreates it, and
|
||||||
|
refuses a name that is not a capability's. A privileged container stays undeclarable.
|
||||||
|
|
||||||
|
**4. ADR 0168's "by hand" is read as "by the operator, through the seat".** Removing what the mesh
|
||||||
|
reports as *other* is still the operator's act and is still never the mesh's own doing; the verb is
|
||||||
|
how the act reaches the machine, recorded on the bus like every other, instead of a shell.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The seat's row gains the three verbs; a mesh that already runs widens its row at the next
|
||||||
|
controller start. The nftables module claims them and gains a runtime — a tool server with the
|
||||||
|
packet filter's tools in its image, on the machine's network, with `NET_ADMIN`.
|
||||||
|
- The host's container vocabulary grows by `capabilities`; an older host refuses a declaration that
|
||||||
|
carries it, so the host rolls before the module.
|
||||||
|
- The two machines of this mesh that ADR 0168 found not filtered by the mesh alone are cleaned
|
||||||
|
through `remove`, and read *the mesh alone* afterwards; `status` returns to well without a hand on
|
||||||
|
either machine.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| The seat declares the three verbs; a claim that serves fewer is refused by name | the catalogue's seat tests |
|
||||||
|
| `remove` refuses the mesh's tables, the runtime's chains, a built-in chain and an active front end's chains, and removes a user chain with its jumps, empties the user chain, deletes an own table | the module's tests over a fake command runner, with the shapes the host reported live |
|
||||||
|
| A container's capabilities reach the runtime and its spec; an unknown name is refused | host tests |
|
||||||
|
| Live | `node-packet-filter.remove@<node>` on the home server and the control node; `node show` reads *the mesh alone* on both; `status` is well |
|
||||||
|
|
||||||
|
## Built and proven live, 2026-10-02
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-02.** The decision stands; these are the facts of its building.
|
||||||
|
> Written as 0169 for three hours and renumbered to 0170: another record took 0169 on main first,
|
||||||
|
> and the check that refuses a shared number covered issues only (now records too).
|
||||||
|
|
||||||
|
Built in mesh-host 68 (`capabilities` on a container), mesh-controller 212 (the seat's three verbs)
|
||||||
|
and 213 (the filter file a module names under `filtering.into` counts as declared for a mount — the
|
||||||
|
module's first build was refused without it), mesh-catalog 216 (the nftables module's runtime and
|
||||||
|
verbs) and mesh-tools 27 (the console lists a node-scoped seat's verbs with their scope and carries the
|
||||||
|
machine; before it, the verbs were live on four machines and unreachable from the console —
|
||||||
|
[issue 199](../04-ISSUES/199-a-node-scoped-seats-verb-could-not-be-called-through-the-console/00-report.md)).
|
||||||
|
Each machine's holder was issued its bus account with `mesh-controller.issue`, the broker node pushed
|
||||||
|
first. At 12:46Z the five rule sets ADR 0168 had named were removed through
|
||||||
|
`node-packet-filter.remove`, three on the home server and two on the control node, each answering
|
||||||
|
with the commands it ran; the next report read *the mesh alone* on all four machines and `status`
|
||||||
|
listed nothing under `filtered`. The live row is read. What it cost on the way is
|
||||||
|
[issue 200](../04-ISSUES/200-the-controllers-answer-to-the-console-is-refused-by-the-bus/00-report.md).
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
|
||||||
|
- [Design 33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md), [Design 08 — Connectivity](../03-DESIGN/01-to-be/08-connectivity.md)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0016-the-lab.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 172. The lab is a module, and runs a bed when the mesh asks
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The lab raises virtual machines and runs the mesh on them, end to end, before a change reaches a real
|
||||||
|
machine ([ADR 0016](0016-the-lab.md)). It runs on one machine of the mesh, the one with the
|
||||||
|
virtualisation it needs. Until now the only way to start a bed there was to sign in to that machine and
|
||||||
|
run the lab's command line by hand, with a dozen environment variables pointing at sibling checkouts.
|
||||||
|
|
||||||
|
Nothing in the mesh could ask for it. An agent working through the mesh's own tools could build,
|
||||||
|
merge and push a change, and could not prove it in the lab first. The operator's direction on
|
||||||
|
2026-10-02: work on another machine goes through a mesh tool, not a shell on it.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep the lab a command line on one machine.** Every run is a person, or an agent with a shell on
|
||||||
|
that machine, outside the mesh.
|
||||||
|
2. **The lab is a module.** Assigned to the machine that can run it, serving tools that run a bed
|
||||||
|
against named branches and say how it went.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Option 2.**
|
||||||
|
|
||||||
|
- **A `lab` module, assigned where the lab can run**, serves five tools: whether this machine can run
|
||||||
|
beds, run beds against a branch per repository, a run's state, its log, and stopping it.
|
||||||
|
- **A run is the lab's own suite**, against fresh checkouts of the named branches from the mesh's forge,
|
||||||
|
side by side as the lab expects them. It builds what the beds place from those checkouts, as the suite
|
||||||
|
already does. It answers at once with an id, like a build: a bed takes minutes, and a call does not.
|
||||||
|
- **Only branches on the forge are run**, never code handed to the tool. What a run tested is what the
|
||||||
|
forge holds at the commit it names.
|
||||||
|
- **The lab is reached over the mesh only.** Its tools travel the bus, and the module opens no port.
|
||||||
|
- **No grant beyond the mesh's own.** Running a bed is root on the lab's machine, but anyone who can call
|
||||||
|
the mesh's tools can already do worse. The operator's judgement on 2026-10-02.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- An agent proves a change in the lab through the mesh, the same way it builds and pushes one.
|
||||||
|
- The lab's machine carries a module whose runtime holds the virtualisation's and the container
|
||||||
|
runtime's sockets, and a toolchain to build the mesh with.
|
||||||
|
- A run's checkouts are its own, so two runs never build from each other's tree. Old ones are removed
|
||||||
|
when their run ends.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A run checks out exactly the named branches, and reports the commits it tested | the module's tests over a forge fixture, and each run's answer |
|
||||||
|
| A run answers at once, and its state and log follow it to the end | by hand, the first run |
|
||||||
|
| The module opens no port | the composed filter of the lab's machine |
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0040-what-a-module-is.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 173. The operator's machine is the mesh's, and a module is whatever it declares
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0040](0040-what-a-module-is.md) says a module is *one self-contained piece of software the
|
||||||
|
mesh installs and manages*, and every example it gives is a service: a database, an analytics
|
||||||
|
server, a forge. The catalogue followed the examples. Of the predecessor's 34 modules on one
|
||||||
|
workstation, 28 are the operator's environment — a login manager, a window manager with 88 files
|
||||||
|
and four flavors, a shell, a terminal, a launcher, an audio setup, scripts — and the migration
|
||||||
|
scoped all 28 out as *the workstation's own environment*, to be managed by nobody
|
||||||
|
([research 018](../01-RESEARCH/018-the-operators-machine-as-modules/02-what-exists-and-what-is-missing.md)).
|
||||||
|
Since the predecessor retired, nobody is exactly who manages them: a fix is a hand edit that
|
||||||
|
nothing records and nothing regenerates.
|
||||||
|
|
||||||
|
[To-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) reached under the home for
|
||||||
|
one directory and drew a boundary inside it. The operator's statement is wider: *the mesh manages
|
||||||
|
my entire machine, all four of them, as far as it makes sense* — system folders and the home
|
||||||
|
alike, the servers and the workstations from the same catalogue. And the operator refused a
|
||||||
|
distinction this effort first drew between modules that ship code and modules that ship only
|
||||||
|
declarations: *a module can have some tools, a seat implementation, some containers, a unit, a
|
||||||
|
binary, some config files — one of these, or all, or two.*
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep 0040's reading and manage the environment outside the catalogue** — dotfiles in a
|
||||||
|
repository, a script that places them. Rejected: that is the predecessor's first two days, the
|
||||||
|
origin of every inherited shape [as-is 10](../03-DESIGN/00-as-is/10-module-catalogue.md)
|
||||||
|
documents, and it puts the one thing a person looks at outside the one mechanism that is
|
||||||
|
checked.
|
||||||
|
2. **Add a second kind of module for configuration** — a "config module" with files and no
|
||||||
|
process. Rejected by the operator: a kind is a distinction the manifest already makes by what
|
||||||
|
it declares, and a second kind is a second set of rules to keep in step.
|
||||||
|
3. **One definition: a module is one managed thing, described by what it declares.** Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. Everything configurable on a node is declared by a module.** Services, and equally the login
|
||||||
|
manager, the display server, the window manager, the shell, the terminal, the launcher, the
|
||||||
|
notifier, the audio setup, the boot images, the package manager's configuration, the agent at the
|
||||||
|
terminal, and a folder a person works in. The test is *can it be configured on a machine*; if it
|
||||||
|
can, some module owns it. What no module declares is found and left alone, as adoption already
|
||||||
|
says of a machine ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
|
||||||
|
|
||||||
|
**2. A module is whatever it declares, and there are no kinds of module.** A package, files, a
|
||||||
|
container, a unit, a binary, a seat claim, tools — any one, or all. 0040's *one self-contained piece
|
||||||
|
of software* stands; its examples were services, and that was the whole of the bias. A downloads
|
||||||
|
folder with a process that tidies it, backs it up and answers questions about it is a piece of
|
||||||
|
software by 0040's own test, and so is a shell that is a package, three files and a seat.
|
||||||
|
|
||||||
|
**3. The home has no boundary of its own.** A file under the operator's home is placed and owned
|
||||||
|
the way [to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §2 built it: by a
|
||||||
|
module, resolved against the account's home, owned by the account. Which files are the mesh's is
|
||||||
|
decided by what modules declare, not by a line drawn through a directory. A person's documents,
|
||||||
|
projects and history are data under [ADR 0051](0051-shared-data-is-the-operators.md) and no module
|
||||||
|
declares them.
|
||||||
|
|
||||||
|
**4. One module ships one default configuration.** No flavors. What differed between the
|
||||||
|
predecessor's four flavors of one desktop module is what [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)
|
||||||
|
is for.
|
||||||
|
|
||||||
|
**5. Servers and workstations take the same catalogue.** A module declares what it needs; a
|
||||||
|
machine reports what it has; assignment refuses by name
|
||||||
|
([ADR 0161](0161-what-deserves-a-seat.md) §3). The shell, the prompt, git and the agent are universal.
|
||||||
|
A display server needs a graphical session; a window manager needs the display server held. Nothing
|
||||||
|
in a manifest says *workstation*.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The catalogue grows by a family of modules that run no service. Each is still built,
|
||||||
|
registered, assigned, pushed and reported like every other, and `status` says whether a
|
||||||
|
machine has applied them.
|
||||||
|
- The account fact becomes load-bearing for every node a person uses. Today it is empty on all
|
||||||
|
four node records of this mesh; stating it is the first step of the build.
|
||||||
|
- A module that *installs* a thing is distinct from a module that *holds its role*: zsh, fish and
|
||||||
|
bash may all be installed, and one holds the login shell
|
||||||
|
([ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)).
|
||||||
|
- The host's `package` shape drives the distribution's package manager only. A module whose
|
||||||
|
package is outside the distribution's repositories — the login manager in use is one — needs
|
||||||
|
either an official package or a shape the host does not have. Recorded as a gap, not decided.
|
||||||
|
- The predecessor's hooks go. What they did becomes declared state the host applies, or a verb a
|
||||||
|
seat serves ([ADR 0177](0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)).
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A manifest with no container, no unit and no binary registers and resolves like any other | the catalogue's registration tests, with a package-and-files manifest |
|
||||||
|
| A file resource under the home resolves against the account and is owned by it | the controller's composition tests (to-be 29 §2, built) |
|
||||||
|
| A home-scoped module is refused on a node with no account, naming the fact | the same tests |
|
||||||
|
| A module needing a capability the machine lacks is refused by name | the resolver's tests (ADR 0161 §3) |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md), documents
|
||||||
|
01 and 02 — the behaviour wanted and the inventory measured.
|
||||||
|
- [ADR 0040](0040-what-a-module-is.md), [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md),
|
||||||
|
[ADR 0051](0051-shared-data-is-the-operators.md), [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)
|
||||||
|
- [To-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) — the account and the home
|
||||||
|
as a placement root, built; the records for them are proposed in an open change.
|
||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
topic: building it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0011-managed-files-are-generated-never-edited.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 174. A node varies a module through settings and kept regions, never through an edit
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0011](0011-managed-files-are-generated-never-edited.md) says a managed file is derived and an
|
||||||
|
edit to it is overwritten without warning. The predecessor said the same and then undid it twice:
|
||||||
|
a `merge` strategy that adopted disk drift back into its database, so a local edit became the
|
||||||
|
record; and a theming layer of about 90 environment variables substituted into templates at sync
|
||||||
|
time, with tools to list and set them, so that *nearly every value was a variable* — a second
|
||||||
|
configuration language laid over the first.
|
||||||
|
|
||||||
|
The operator wants both the variation and the rule. One window-manager module with one default
|
||||||
|
configuration, and each node tweaking it; and the file carrying the wanted value rather than a
|
||||||
|
variable the file reads. Two mechanisms already exist for exactly this: a **setting**, declared by
|
||||||
|
the module and set per mesh or per node, rendered at composition
|
||||||
|
(`${setting:…}` is live in the resolver's manifest); and a **kept region**, a block in a file the
|
||||||
|
mesh writes *into* where the operator's own lines survive every push
|
||||||
|
([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), used by the ssh-client module
|
||||||
|
for the operator's own `Host` blocks).
|
||||||
|
|
||||||
|
What stands in the way is [issue 168](../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md):
|
||||||
|
a setting today reaches every mergeable file and every contribution of its module. Ninety theme
|
||||||
|
knobs on that mechanism would reach ninety files. The record that fixes it — a setting declared
|
||||||
|
with its type, meaning, default and the file it lands in — is proposed in an open change alongside
|
||||||
|
the container-runtime records.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Carry the predecessor's merge strategy.** A local edit is adopted into the node's layer.
|
||||||
|
Rejected: two writers and no arbiter, which is the option 0011 removed, and the reason a
|
||||||
|
`/model` choice was silently reverted on every node for weeks before anyone found the cause.
|
||||||
|
2. **Carry the environment-variable theming.** Rejected by the operator: the value belongs in
|
||||||
|
the file; a variable the file reads is a second place for the same fact.
|
||||||
|
3. **A per-node file override** — a whole file replaced for one node. Rejected: it is a flavor
|
||||||
|
under another name, and a module update then misses that node entirely.
|
||||||
|
4. **Settings rendered into the file, and kept regions, and nothing else.** Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**A node varies a module in exactly two ways.**
|
||||||
|
|
||||||
|
- **A setting.** Declared by the module with a default, set for the mesh or for one node, rendered
|
||||||
|
into the file at composition. The value is in the file. Asked, the mesh lists every setting
|
||||||
|
with its effective value and where it came from.
|
||||||
|
- **A kept region.** A marked block in a file the mesh writes into, in which the operator's own
|
||||||
|
lines are kept across every push and given back when the module goes (ADR 0102).
|
||||||
|
|
||||||
|
**An edit outside a kept region is overwritten, as ADR 0011 says, and never adopted.** Nothing
|
||||||
|
reads a managed file back into the record.
|
||||||
|
|
||||||
|
**The predecessor's theme knobs become settings** of the modules whose files they render — the
|
||||||
|
window manager's colours are the window manager's settings, the bar's are the bar's — each
|
||||||
|
landing in the file that reads it and no other.
|
||||||
|
|
||||||
|
**Issue 168 is fixed before any environment module declares a setting.** A setting must name the
|
||||||
|
file it lands in; until that ships, the environment modules carry their defaults in their files
|
||||||
|
and no settings.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- No flavors, no per-node file copies, no environment layer. A module's definition is one set of
|
||||||
|
files; a node's difference is data in its layer, visible by asking.
|
||||||
|
- The settings record proposed alongside the container-runtime records is on the critical path
|
||||||
|
of every module with a knob, and this record depends on it shipping as proposed.
|
||||||
|
- A kept region is the only place a person edits a managed file, and the file says where it is.
|
||||||
|
The operator's own prompt customisations, aliases and window rules live there.
|
||||||
|
- What got harder: a change that is neither a setting the module declared nor the operator's own
|
||||||
|
lines has no home, and is refused by the mechanism rather than silently kept. That is the point.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A setting reaches only the file its declaration names | the controller's settings tests, once the proposed record ships; issue 168 closes on it |
|
||||||
|
| A kept region survives a push with its content and is given back on undeclare | the host's write-into tests (ADR 0102), with a region declared by an environment module |
|
||||||
|
| An edit outside a region does not survive a push | the same tests, asserting the file equals the composed content outside the region |
|
||||||
|
| Every effective value names its source | `mesh-controller.settings` and the module's own `show-config` tool |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/01-the-intended-behaviour.md) §"One default, varied by settings, never by edits"
|
||||||
|
- [ADR 0011](0011-managed-files-are-generated-never-edited.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md),
|
||||||
|
[issue 168](../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 175. One tool runtime per node serves every module's tools, on the host side
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
A module's tools are code the module wrote, one function behind each verb, served on the subjects
|
||||||
|
the controller issues in the module's membership
|
||||||
|
([ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)).
|
||||||
|
What *runs* that code is [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md):
|
||||||
|
a supervised process per module under the module's own account, and in the catalogue as built,
|
||||||
|
that process is a container per module per node, built on the tool runtime's base image.
|
||||||
|
|
||||||
|
Measured on the live mesh ([research 018](../01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md)):
|
||||||
|
67 module tools, each served from its module's container; the packet-filter seat's three verbs
|
||||||
|
served by a container with `NET_ADMIN` on every one of four machines, for a module that is
|
||||||
|
otherwise a package, three files and a service; and the console, a container per node, calling
|
||||||
|
everything and serving nothing. The operator's environment adds a dozen modules of the
|
||||||
|
packet-filter shape, and the operator's judgement is plain: *I would never run MCP tools inside
|
||||||
|
a container; that is a very bad design.* And: *I don't care about permissions or account per
|
||||||
|
module, that just complicates things for no good reason. Just a node-level tool executor. If a
|
||||||
|
command needs root, that's the module's concern.*
|
||||||
|
|
||||||
|
The tool runtime itself was written for this. Its own description: *the per-node process that
|
||||||
|
makes a module's tools actually serve — imports the assigned modules' compiled tool entrypoints,
|
||||||
|
each of which registers its tools as it loads; on a node the host resolves the list and starts it
|
||||||
|
like any other supervised workload.* What the catalogue did instead was build one image per module
|
||||||
|
around it.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep a process per module.** Rejected: one container per module per node for software that
|
||||||
|
is not a container, and the account-per-module invariant it exists to protect is one the
|
||||||
|
operator declines to pay for.
|
||||||
|
2. **The host executes tools itself.** Rejected: the host is a static Go binary that loads no
|
||||||
|
plugins; a module's tools are TypeScript on the SDK, and building a second SDK in Go for the
|
||||||
|
host's sake is the cost ADR 0039 refuses.
|
||||||
|
3. **One tool runtime per node, a sibling of the host, loading every assigned module's bundle.**
|
||||||
|
Chosen. It is what the runtime was written to be.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. One tool runtime per node, supervised by the host, on the host side — never a container.**
|
||||||
|
The host starts it the way the launcher starts the host
|
||||||
|
([ADR 0005](0005-the-node-host.md)): a process on the machine, restarted when it dies. It holds one
|
||||||
|
bus credential, the node's. It is module-agnostic: it knows bundles and subjects, nothing of what
|
||||||
|
any module does.
|
||||||
|
|
||||||
|
**2. It serves every assigned module's tools and every held seat's verbs** on the subjects the
|
||||||
|
memberships issue. ADR 0159 and ADR 0160 are unchanged in what they say about subjects, grants
|
||||||
|
and memberships; what changes is that one process on the node subscribes to all of them instead of
|
||||||
|
one process per module. A module that runs a long-lived service of its own — a daemon, a
|
||||||
|
container — keeps it; this record is about tools.
|
||||||
|
|
||||||
|
**3. A module brings its tools as a bundle**, the artifact kind the catalogue already has for
|
||||||
|
interpreted code, built by the pipeline and delivered to the node by the host as it delivers any
|
||||||
|
artifact. Never an image. The runtime loads each bundle as the membership names it, and a push
|
||||||
|
that adds or replaces a bundle reaches a running runtime as a reload.
|
||||||
|
|
||||||
|
**4. Root is the module's concern.** A tool that must change the packet filter or rebuild boot
|
||||||
|
images escalates itself. The runtime does not run as root for everyone's sake; the caller does not
|
||||||
|
know and need not.
|
||||||
|
|
||||||
|
**5. Any node may call any tool on any node.** The runtime's credential may call everything, as
|
||||||
|
the console's already may. A per-module calling grant is not kept.
|
||||||
|
|
||||||
|
**6. The console is this runtime's serving mode, renamed.** [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)
|
||||||
|
stands in substance — a module assigned per node, MCP on the machine's loopback, the machine's
|
||||||
|
login is the authority — and changes in form: host-side, serving as well as calling, and named for
|
||||||
|
what it is: **node tools**. The mesh's own verbs stay with the controller
|
||||||
|
([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)); a mesh-scoped seat's verbs
|
||||||
|
run on the node that holds it ([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)).
|
||||||
|
|
||||||
|
**Where ADR 0047 and ADR 0150 say a module's tools are served by the module's own process under
|
||||||
|
the module's own account, read this record.** Everything else they decided stands: a tool is served
|
||||||
|
on its own subject, only the module that serves it answers, a module's long-lived processes are the
|
||||||
|
machine's to supervise. The invariant 0150 kept — one account per module — no longer holds for
|
||||||
|
tools, and the reason is stated above: every tool is callable from everywhere by decision 5, so the
|
||||||
|
account no longer scopes anything a caller cannot already reach.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The packet-filter module's container goes; its verbs run on the host side and escalate as they
|
||||||
|
need. [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md) §3's container capability is moot
|
||||||
|
for it.
|
||||||
|
- The tool runtime's base image stays the way a module's *service* may be built; it is no longer
|
||||||
|
the way tools reach a node.
|
||||||
|
- The node tools runtime needs an interpreter on the machine. The module that is the runtime
|
||||||
|
declares it as a package.
|
||||||
|
- The container-runtime seat proposed in an open change says its holder *runs as a supervised
|
||||||
|
process and serves the verbs locally to the host and on the bus*. A supervised process serving
|
||||||
|
verbs is what this runtime is; whether that holder keeps a process of its own or serves through
|
||||||
|
the runtime is for that record's build to say.
|
||||||
|
- What got harder: one process carries every module's tool code on a node, so one module's
|
||||||
|
faulty bundle can take down the node's tools. The runtime loads each bundle guarded and names
|
||||||
|
the one that failed; the others serve.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| The runtime loads every bundle its memberships name and serves each tool on its subject | the runtime's tests against a real bus: two bundles, three tools, each answers |
|
||||||
|
| A bundle that fails to load is named and the others serve | the same tests, with one bundle that throws on load |
|
||||||
|
| The host supervises the runtime and restarts it | the host's tests over the launcher's shape |
|
||||||
|
| A push that replaces a bundle reloads it without a restart | the runtime's tests: a bundle replaced on disk, the membership re-read, the new tool answers |
|
||||||
|
| No module in the catalogue declares a container whose only purpose is tools | a catalogue check: a manifest with `tools` and an image artifact built on the tool runtime's base is refused once the runtime is live |
|
||||||
|
| Live | `login-shell.execute@<node>` answers on every node from the node tools runtime; `docker ps` shows no per-module tool container |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md)
|
||||||
|
- [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md), [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md),
|
||||||
|
[ADR 0152](0152-the-operators-surface-is-a-module-the-console.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 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
|
||||||
|
- [To-be 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md), [to-be 34](../03-DESIGN/01-to-be/34-the-console.md)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 176. The login shell is a node seat held by one shell module, and `execute` is its contract
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0040](0040-what-a-module-is.md) names the shell as its example of a *shared* seat: bash, zsh
|
||||||
|
and fish all join `shell`, and one may be default. The operator's reading is sharper, and it
|
||||||
|
matches [ADR 0126](0126-a-module-declares-its-own-seats.md) better: *installing* a shell is
|
||||||
|
installing software, and several may be installed; *holding* the seat is being the login shell,
|
||||||
|
which a node has exactly one of. A definition says which seats a module can hold; the assignment
|
||||||
|
says which it does.
|
||||||
|
|
||||||
|
A seat carries the tools its holder must serve ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)),
|
||||||
|
and [to-be 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) leaves which verbs each
|
||||||
|
seat serves as a decision per seat, taken slowly. This is the first seat of the operator's
|
||||||
|
environment, and the one every node has.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **A shared `shell` seat with a default**, as 0040's example reads. Rejected: *default* is a
|
||||||
|
second concept beside *holder* for the same fact, and the `user` shape already makes the
|
||||||
|
login shell declared state ([to-be 05](../03-DESIGN/01-to-be/05-the-node-host.md)).
|
||||||
|
2. **No seat; each shell module sets the login shell for itself.** Rejected: two assigned shell
|
||||||
|
modules would fight over `chsh`, and nothing would say which won.
|
||||||
|
3. **An exclusive node-scoped seat, `login-shell`, declared by the shell modules, held by one
|
||||||
|
per node.** Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. `login-shell` is a node-scoped seat declared by the shell modules.** zsh, fish and bash each
|
||||||
|
declare that they can hold it; a node's assignment says which does; the controller refuses a
|
||||||
|
second holder by name as for every seat. A shell module that is assigned without holding the seat
|
||||||
|
is installed and nothing more.
|
||||||
|
|
||||||
|
**2. Holding the seat sets the account's login shell.** The holder's declaration carries the
|
||||||
|
`user` shape with the shell it provides, so the login shell is declared state the host applies and
|
||||||
|
gives back when the holding moves — `chsh` stops being a hook.
|
||||||
|
|
||||||
|
**3. The seat's contract is `execute`.** One verb, one argument, the command, run on the node the
|
||||||
|
seat is scoped to as the operator account, answering with what it printed and how it exited.
|
||||||
|
Every holder serves it; a holder may serve its own tools beside it
|
||||||
|
([ADR 0170](0170-the-firewall-seat-serves-its-verbs.md) §2) — show the rendered configuration, list
|
||||||
|
the plugins, set a prompt value.
|
||||||
|
|
||||||
|
**4. Any node may call it on any node.** The grant is the node tools runtime's
|
||||||
|
([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) §5):
|
||||||
|
*run `uptime` on every node* is five calls to one verb.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The first environment module is a shell: a package, files under the home owned by the account,
|
||||||
|
a seat declaration and claim, a `user` shape, and one tool. It proves the whole pattern on every
|
||||||
|
node, servers included, before anything graphical is written.
|
||||||
|
- ADR 0040's shell example is read as *installed is not holding*; a dated note in that record says
|
||||||
|
so. Its decision is untouched.
|
||||||
|
- `execute` is a shell on every machine, addressed over the bus. That is the point, and it is
|
||||||
|
the widest verb the mesh serves; it exists because the operator decided every node may call
|
||||||
|
every tool, and this record does not narrow that.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| Two shell modules assigned to one node, one holding: one `user` shape in the declaration, naming the holder's shell | the controller's composition tests |
|
||||||
|
| A second claimant is refused by name | the catalogue's seat tests |
|
||||||
|
| `execute` runs as the account and answers output and exit status | the module's tool tests over a fake runner, and live on every node |
|
||||||
|
| The seat's verb appears with its scope and machine in the node tools listing | the runtime's tests (to-be 33 §4) |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md)
|
||||||
|
- [ADR 0040](0040-what-a-module-is.md), [ADR 0126](0126-a-module-declares-its-own-seats.md),
|
||||||
|
[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 177. A unit may be user-scoped, and the service manager is a node seat whose holder answers for the units
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The host's `service` shape puts a system unit into a state. It has no user scope.
|
||||||
|
[To-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) states the gap: *a
|
||||||
|
workstation's per-user daemons have no form the mesh can send.* Four of the predecessor's
|
||||||
|
environment modules ship user units — the desktop's reload watcher and bar watchdog, the audio
|
||||||
|
module's masks, the power module's memory guard, the thermal daemon's profile switcher — and the
|
||||||
|
predecessor needed a hook to enable them because *shipping a unit file does not run it*; one unit
|
||||||
|
was deployed for months and ran on one machine only.
|
||||||
|
|
||||||
|
[ADR 0040](0040-what-a-module-is.md) says the host hardcodes no supervisor, and a swappable
|
||||||
|
machine mechanism is a module implementing a capability — which is what the nftables module is for
|
||||||
|
the packet filter ([ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)). The service manager is
|
||||||
|
reported today as a capability, `service-manager`, and held by nobody. The operator's proposal: a
|
||||||
|
systemd module that holds the seat and serves the tools about units, system and user.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep user units as a module concern** — each module runs `systemctl --user` in a hook.
|
||||||
|
Rejected: that is the hook that silently never ran, and an action over the link is refused.
|
||||||
|
2. **The service-manager module applies units** on behalf of others, as a provision. Rejected by
|
||||||
|
the operator: provisioning is for resources a provider creates for a consumer; a unit is
|
||||||
|
declared state the host applies, as every resource is.
|
||||||
|
3. **The host's `service` shape gains a user scope; a systemd module holds the service-manager
|
||||||
|
seat and serves the verbs about units.** Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. The `service` shape gains `scope`: `system` (the default) or `user`.** A user-scoped unit
|
||||||
|
is applied as the operator account through the account's own service manager: enabled, started,
|
||||||
|
stopped, reloaded on its triggers, exactly as a system unit is, and refused on a node with no
|
||||||
|
account, naming the fact. The host applies it; no module does.
|
||||||
|
|
||||||
|
**2. `node-service-manager` is a seat of the mesh's own, node-scoped**, seeded by the controller
|
||||||
|
under this record, as ADR 0121 requires of a `node-*` name. The `systemd` module claims it and is
|
||||||
|
assigned to every machine whose profile reports `service-manager`.
|
||||||
|
|
||||||
|
**3. The seat's verbs answer for every unit on the machine**, each taking an optional `scope`:
|
||||||
|
`units`, `status`, `start`, `stop`, `restart`, `enable`, `disable`, `journal`. The host applies what
|
||||||
|
is declared; the holder answers questions and operator acts about it, and says, for a mesh-held
|
||||||
|
unit, that the host will restore what its declaration says.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The host's vocabulary grows by one field on one shape, asserted by its count test
|
||||||
|
([to-be 05](../03-DESIGN/01-to-be/05-the-node-host.md)); an older host refuses a declaration
|
||||||
|
carrying it, so the host rolls before the first module that uses it.
|
||||||
|
- The predecessor's four user-unit modules become declarable without a hook.
|
||||||
|
- The seat's holder is the first system seat held by a module that runs nothing of its own: its
|
||||||
|
verbs are served by the node tools runtime ([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
|
||||||
|
- What got harder: `journal` and `status` on a user unit need the account's manager reachable
|
||||||
|
from the runtime's process, which runs as the node's account; the holder's tool escalates or
|
||||||
|
switches user as it needs, which is ADR 0175 §4 applied.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A `service` with `scope: user` is enabled and started under the account, and refused with no account | the host's tests with a fake service manager |
|
||||||
|
| The seat declares its verbs; a claim serving fewer is refused by name | the catalogue's seat tests |
|
||||||
|
| The verbs act on a named unit in the named scope and name the unit's holder when the mesh declares it | the module's tests over a fake runner |
|
||||||
|
| Live | the desktop's reload watcher declared `scope: user` on a workstation; `node-service-manager.status@<node>` reports it active |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md)
|
||||||
|
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md),
|
||||||
|
[ADR 0040](0040-what-a-module-is.md), [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)
|
||||||
|
- [To-be 05](../03-DESIGN/01-to-be/05-the-node-host.md), [to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 180. The found front end is uninstalled once a machine is converged
|
||||||
|
|
||||||
|
> **Renumbered 2026-10-02.** Written and merged as 0175 while another record already held that number on main (one tool runtime per node, merged minutes earlier); `cycle.py` refused main. The branch that lands last renumbers: 0178 and 0179 are claimed by open changes, so this is 0180. Nothing cited it by number.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) retires the firewall a machine
|
||||||
|
was found with by disabling it, never flushing it, and keeps its configuration on disk so that
|
||||||
|
returning the node to adopted can enable it again. [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)
|
||||||
|
made the host keep it retired and say so. Both machines of this mesh that had a front end have been
|
||||||
|
converged for days; neither is going back. What remained of the front end on each — its package,
|
||||||
|
its unit enabled for boot on one, its empty chains still wired into the kernel's hooks, a chain of
|
||||||
|
its container integration still dropping traffic on the IPv6 path until the day before this record
|
||||||
|
— was not a rollback path. It was software nobody runs, left where a reader finds it and asks
|
||||||
|
whether the machine has two firewalls.
|
||||||
|
|
||||||
|
The operator asked on 2026-10-02 that it be disabled and uninstalled. Disabled it already was. For
|
||||||
|
uninstalled, the host had no word: a package could be declared present and not absent.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. A package may be declared absent.** `absent: true` on a package resource has the host remove
|
||||||
|
the package when it is installed and leave alone a machine that never had it, through the machine's
|
||||||
|
own package manager, dependencies untouched. A declaration that stops saying a package is absent
|
||||||
|
installs nothing: there is nothing to undo.
|
||||||
|
|
||||||
|
**2. The module that holds the packet filter seat declares the front end it replaced absent**, after
|
||||||
|
its own filter is loaded, so the mesh's table is in force before the front end's package goes. On a
|
||||||
|
converged machine the front end is therefore gone, not merely off; on an adopted machine nothing of
|
||||||
|
this runs, because the filter module is assigned by the flip and not before.
|
||||||
|
|
||||||
|
**3. Returning such a machine to adopted enables nothing.** A machine with no firewall needs no
|
||||||
|
openings ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)); the host records the
|
||||||
|
front end as *removed*, says so once, and asks nothing of a command that is not there. What
|
||||||
|
ADR 0100 kept on disk for a return is kept only as far as the package manager keeps a changed
|
||||||
|
configuration file; the rollback path it described is given up on purpose.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The host's vocabulary grows by `absent` on a package; an older host refuses a declaration
|
||||||
|
carrying it, so the host rolls before the module.
|
||||||
|
- The nftables module's declaration gains one resource; on the two machines of this mesh that were
|
||||||
|
found with ufw, the next push removes it.
|
||||||
|
- `node show` reads *found firewall: ufw, removed* on those machines from then on.
|
||||||
|
- ADR 0100's sentence about a return to adopted restoring the found firewall holds only while the
|
||||||
|
front end is installed, which after this record it is not on a converged machine.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| An absent package is removed when present, left when not, and read back | host tests over a fake package manager |
|
||||||
|
| An uninstalled front end is recorded as removed and nothing is asked of it | a host test with ufw missing on a converged apply |
|
||||||
|
| Live | the two machines report ufw gone: `pacman -Q ufw` has no answer, `node show` says removed, `status` is well |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)
|
||||||
|
- [Design 08 — Connectivity](../03-DESIGN/01-to-be/08-connectivity.md)
|
||||||
@@ -169,6 +169,20 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0134** — [The mesh says what it applied](0134-the-mesh-says-what-it-applied.md)
|
- **0134** — [The mesh says what it applied](0134-the-mesh-says-what-it-applied.md)
|
||||||
- **0142** — [The mesh delivers its own components as binaries, not as container images](0142-the-mesh-delivers-its-own-components-as-binaries.md)
|
- **0142** — [The mesh delivers its own components as binaries, not as container images](0142-the-mesh-delivers-its-own-components-as-binaries.md)
|
||||||
- **0154** — [The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
|
- **0154** — [The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
|
||||||
|
- **0156** — [An artifact is what a build produces, the artifact store serves every kind, and its seat is named for its scope](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)
|
||||||
|
- **0157** — [A build says what it does on the bus, as it happens](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)
|
||||||
|
- **0158** — [A provider with one credential shares it with every consumer, and the vault remakes it for all of them at once](0158-a-provider-with-one-credential-shares-it-with-every-consumer.md)
|
||||||
|
- **0159** — [A tool call names the machine it is for, every answer says which machine answered, and a holder's runtime serves its seat's verbs](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
|
||||||
|
- **0160** — [The mesh issues an assignment's subjects, and a runtime serves what it is issued](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||||
|
- **0161** — [What deserves a seat: a role of a module is a seat, a singular fact about machines is a placement with a capacity of one, and a holder's software is the machine's](0161-what-deserves-a-seat.md)
|
||||||
|
- **0162** — [A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md)
|
||||||
|
- **0163** — [Taking a module over is a comparison: what it compares, what it refuses, and what it carries](0163-taking-a-module-over-is-a-comparison.md)
|
||||||
|
- **0167** — [A membership carries what its module receives, and who the mesh is](0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md)
|
||||||
|
- **0168** — [A converged machine is filtered by the mesh alone, and the host says what else refuses](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)
|
||||||
|
- **0169** — [A machine joins through the tunnel, and the bus is never public](0169-a-machine-joins-through-the-tunnel-and-the-bus-is-never-public.md)
|
||||||
|
- **0170** — [The firewall seat serves its verbs, and a foreign rule set is removed through one of them](0170-the-firewall-seat-serves-its-verbs.md)
|
||||||
|
- **0172** — [The lab is a module, and runs a bed when the mesh asks](0172-the-lab-is-a-module-and-runs-a-bed-when-the-mesh-asks.md)
|
||||||
|
- **0180** — [The found front end is uninstalled once a machine is converged](0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md)
|
||||||
|
|
||||||
### Its tiers, from the bottom up
|
### Its tiers, from the bottom up
|
||||||
|
|
||||||
@@ -258,6 +272,11 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0147** — [A module anchors the mesh's authority on a machine, and takes it away again](0147-a-module-anchors-the-meshs-authority.md)
|
- **0147** — [A module anchors the mesh's authority on a machine, and takes it away again](0147-a-module-anchors-the-meshs-authority.md)
|
||||||
- **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)
|
- **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)
|
- **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)
|
||||||
|
- **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)
|
||||||
|
- **0177** — [A unit may be user-scoped, and the service manager is a node seat whose holder answers for the units](0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)
|
||||||
|
|
||||||
### How it is built
|
### How it is built
|
||||||
|
|
||||||
@@ -279,6 +298,7 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0107** — [Persistent data is a directory bind, never a named volume](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)
|
- **0107** — [Persistent data is a directory bind, never a named volume](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)
|
||||||
- **0111** — [A build source is on the mesh's git seat, or it is an external repository](0111-a-build-source-is-on-the-git-seat-or-external.md)
|
- **0111** — [A build source is on the mesh's git seat, or it is an external repository](0111-a-build-source-is-on-the-git-seat-or-external.md)
|
||||||
- **0149** — [The live mesh is the test bed](0149-the-live-mesh-is-the-test-bed.md)
|
- **0149** — [The live mesh is the test bed](0149-the-live-mesh-is-the-test-bed.md)
|
||||||
|
- **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)
|
||||||
|
|
||||||
### How it is checked
|
### How it is checked
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ The console asks the `mesh-controller` seat's `tools` verb beside the modules an
|
|||||||
tools as `<seat>.<verb>` — `mesh-controller.status`, `mesh-controller.push` and the other ten. A call
|
tools as `<seat>.<verb>` — `mesh-controller.status`, `mesh-controller.push` and the other ten. A call
|
||||||
to `<prefix>.<name>` reaches the seat when the prefix is a seat declaring that verb, and the module
|
to `<prefix>.<name>` reaches the seat when the prefix is a seat declaring that verb, and the module
|
||||||
otherwise; `seat:<seat>.<verb>` says so outright. When the control plane does not answer, the list
|
otherwise; `seat:<seat>.<verb>` says so outright. When the control plane does not answer, the list
|
||||||
names `mesh-controller (seat)` as not answering and carries the modules' tools regardless.
|
names `mesh-controller (seat)` as not answering and carries the modules' tools regardless. The
|
||||||
|
`mesh-controller` *module* is always named as not answering: it serves no module tools, only its seat's.
|
||||||
|
|
||||||
## Around it
|
## Around it
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: in-progress
|
status: in-progress
|
||||||
code: [mesh-lab]
|
code: [mesh-lab, mesh-catalog modules/lab]
|
||||||
updated: 2026-09-11
|
updated: 2026-10-02
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0172-the-lab-is-a-module-and-runs-a-bed-when-the-mesh-asks.md
|
||||||
- 02-DECISIONS/0016-the-lab.md
|
- 02-DECISIONS/0016-the-lab.md
|
||||||
- 02-DECISIONS/0010-delivery.md
|
- 02-DECISIONS/0010-delivery.md
|
||||||
---
|
---
|
||||||
@@ -119,7 +120,25 @@ In order, on a machine with nothing:
|
|||||||
|
|
||||||
6. **Verification**, as above, before anything is raised.
|
6. **Verification**, as above, before anything is raised.
|
||||||
|
|
||||||
## Open
|
## The lab answers the mesh
|
||||||
|
|
||||||
|
*2026-10-02* ([ADR 0172](../../02-DECISIONS/0172-the-lab-is-a-module-and-runs-a-bed-when-the-mesh-asks.md)).
|
||||||
|
Once installed, the lab is also a module: `lab`, assigned to the machine that passed `check`. Its
|
||||||
|
tools run there and nowhere else:
|
||||||
|
|
||||||
|
| tool | does |
|
||||||
|
|---|---|
|
||||||
|
| `lab_check` | the lab's `check`, on this machine |
|
||||||
|
| `lab_run` | fresh checkouts of the named branches from the forge, side by side, then the suite on the named beds; answers with an id |
|
||||||
|
| `lab_status` | where a run is, and how it ended: the commits it tested, passed and failed |
|
||||||
|
| `lab_log` | the run's output so far |
|
||||||
|
| `lab_stop` | ends a run |
|
||||||
|
|
||||||
|
The runtime is a container holding the toolchain the suite builds with. It reaches the
|
||||||
|
virtualisation daemon and the container runtime through their sockets on the machine, so what it
|
||||||
|
raises is what a hand run raises. The prerequisites above stay installed by hand. The module uses
|
||||||
|
them, and never installs them.
|
||||||
|
|
||||||
|
|
||||||
- **Whether the lab's bootstrap may install packages at all**, given that the mesh's rules
|
- **Whether the lab's bootstrap may install packages at all**, given that the mesh's rules
|
||||||
forbid installing by hand. The resolution is probably that the lab's bootstrap *is* the
|
forbid installing by hand. The resolution is probably that the lab's bootstrap *is* the
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: in-progress
|
status: in-progress
|
||||||
code: [mesh-host]
|
code: [mesh-host]
|
||||||
updated: 2026-09-29
|
updated: 2026-10-02
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md
|
||||||
|
- 02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md
|
||||||
- 02-DECISIONS/0141-the-host-delivers-its-own-successor.md
|
- 02-DECISIONS/0141-the-host-delivers-its-own-successor.md
|
||||||
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
||||||
- 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md
|
- 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md
|
||||||
@@ -153,6 +155,36 @@ checked:* unit tests hold the host to keeping a found file and container, conver
|
|||||||
taken, never removing a held file and reporting one that changed; the adoption bed asserts a found
|
taken, never removing a held file and reporting one that changed; the adoption bed asserts a found
|
||||||
file byte for byte unchanged until its module is taken.
|
file byte for byte unchanged until its module is taken.
|
||||||
|
|
||||||
|
**What the host says of a found container, and what it removes** — revision, 2026-10-01
|
||||||
|
([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md)). Its report of a held
|
||||||
|
container carries the image and the image's creation date, the networks it is on and the other
|
||||||
|
containers on each, its mounts and its published ports — the facts a take compares. The host compares
|
||||||
|
every field it writes before calling a container current, volumes and paths included; its record keeps
|
||||||
|
a resource's former targets, removes a container or file it wrote under a name the declaration no
|
||||||
|
longer names, never removes what was found, and reports what runs on the machine that it neither
|
||||||
|
wrote nor holds. *How it is checked:* ADR 0163's table.
|
||||||
|
|
||||||
|
**What the host joins, keeps and raises for a take** — revision, 2026-10-02
|
||||||
|
([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 4, 6 and 7). A
|
||||||
|
container may name networks it also joins once it runs — the found network a per-machine setting keeps
|
||||||
|
for a taken container while a neighbour still resolves it there; joined after the run, part of the
|
||||||
|
container's spec, refused when it cannot be joined. A declaration may name the modules the mesh left
|
||||||
|
out of it because a stored setting cannot compose with the module's definition: the host keeps what it
|
||||||
|
wrote and holds for a left-out module and says so, where absence used to read as removal. And genesis
|
||||||
|
raises the bootstrap forge under the forge module's container name, with the module's image digest and
|
||||||
|
its data directory, so the module holds it by the found rule; the network is the one difference a take
|
||||||
|
has left to say. *How it is checked:* a host test joins a kept network and refuses one it cannot; a
|
||||||
|
host test keeps a left-out module's record and hold and removes an absent module's; a bootstrap test
|
||||||
|
holds the installer's constants to the module's manifest where the catalogue is checked out beside it.
|
||||||
|
|
||||||
|
**What filters the machine, and the found firewall kept retired** — revision, 2026-10-02
|
||||||
|
([ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)). The host
|
||||||
|
reports, with every apply, every table and legacy chain that refuses traffic and whose it reads it as —
|
||||||
|
the mesh's, the found firewall's, the container runtime's own, a ban, or other — and, converged, whether
|
||||||
|
the firewall it was found with is in force and who retired it. It retires that firewall on every
|
||||||
|
converged apply, not once, records *found inactive* apart from *disabled by the mesh*, and says when the
|
||||||
|
step was skipped. *How it is checked:* ADR 0168's table.
|
||||||
|
|
||||||
**Found reaches every kind that can touch what the machine has**
|
**Found reaches every kind that can touch what the machine has**
|
||||||
([ADR 0103](../../02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)). For a module not yet taken, a directory present with no record
|
([ADR 0103](../../02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)). For a module not yet taken, a directory present with no record
|
||||||
keeps its mode and owner, a unit present with no record keeps its state and boot setting, a
|
keeps its mode and owner, a unit present with no record keeps its state and boot setting, a
|
||||||
|
|||||||
@@ -7,8 +7,13 @@ code:
|
|||||||
- mesh-controller internal/identity/authority.go
|
- mesh-controller internal/identity/authority.go
|
||||||
- mesh-host internal/identity/serving.go
|
- mesh-host internal/identity/serving.go
|
||||||
- mesh-host internal/apply (the service that reflects a rule set)
|
- mesh-host internal/apply (the service that reflects a rule set)
|
||||||
updated: 2026-09-30
|
updated: 2026-10-02
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md
|
||||||
|
- 02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md
|
||||||
|
- 02-DECISIONS/0169-a-machine-joins-through-the-tunnel-and-the-bus-is-never-public.md
|
||||||
|
- 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md
|
||||||
|
- 02-DECISIONS/0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md
|
||||||
- 02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md
|
- 02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md
|
||||||
- 02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md
|
- 02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md
|
||||||
- 02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md
|
- 02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md
|
||||||
@@ -171,6 +176,28 @@ the broker's node must be dialable by every node, at a stable address, and so mu
|
|||||||
reachable; on one network it does not. A mesh whose nodes are all behind NAT cannot be raised, and
|
reachable; on one network it does not. A mesh whose nodes are all behind NAT cannot be raised, and
|
||||||
a broker node whose address moves invalidates every token issued for it.
|
a broker node whose address moves invalidates every token issued for it.
|
||||||
|
|
||||||
|
*2026-10-02.* **The order changes at step 1: the tunnel comes first, from the token**
|
||||||
|
([ADR 0169](../../02-DECISIONS/0169-a-machine-joins-through-the-tunnel-and-the-bus-is-never-public.md)).
|
||||||
|
The circularity above is real, and it is broken differently. The overlay is configured by the mesh,
|
||||||
|
except for the one peer a joining machine needs, and the token carries that peer. So the sequence
|
||||||
|
becomes:
|
||||||
|
|
||||||
|
```
|
||||||
|
0 the node has an underlay address the machine's own
|
||||||
|
1 the node makes its tunnel key before any token; it prints the public half
|
||||||
|
2 a token is issued for that key its address assigned, and the hub sent it as a peer
|
||||||
|
3 the tunnel comes up to the hub from the token alone: the hub's endpoint and key, its address
|
||||||
|
4 the node dials the bus OVER THE TUNNEL, at the bus's private address
|
||||||
|
5 it proves itself, and is proved to enrolment, checking the key is the one the token named
|
||||||
|
6 the rest of the overlay the whole peer set, delivered as files
|
||||||
|
7 names, filtering, routes as before
|
||||||
|
```
|
||||||
|
|
||||||
|
The link no longer stays on the underlay. The bus is reached over the tunnel by every machine,
|
||||||
|
including one that is joining, so it is never opened to the internet. The precondition becomes: **the
|
||||||
|
hub's tunnel must be dialable by every node, at a stable address.** That port answers nothing to a
|
||||||
|
key it does not know.
|
||||||
|
|
||||||
**Whether the link should later move onto the overlay, with the underlay as fallback, is
|
**Whether the link should later move onto the overlay, with the underlay as fallback, is
|
||||||
[open](../../02-DECISIONS/0007-connectivity.md).** It is a decision rather than a derivation: the
|
[open](../../02-DECISIONS/0007-connectivity.md).** It is a decision rather than a derivation: the
|
||||||
gain is which network carries bytes, not what an attacker can reach, since the link is already
|
gain is which network carries bytes, not what an attacker can reach, since the link is already
|
||||||
@@ -709,6 +736,48 @@ needs no new filter; a declared port is reachable from off the private network a
|
|||||||
not; no address of a machine's own networks appears in a rendered filter, asserted on the text; and a
|
not; no address of a machine's own networks appears in a rendered filter, asserted on the text; and a
|
||||||
machine reporting no outward link is refused in the control plane with its existing filter left alone.
|
machine reporting no outward link is refused in the control plane with its existing filter left alone.
|
||||||
|
|
||||||
|
### A converged machine is filtered by the mesh alone, and the host says what else refuses
|
||||||
|
|
||||||
|
*2026-10-02, [ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md),
|
||||||
|
from [issues 143](../../04-ISSUES/143-converging-does-not-retire-the-firewall-it-found/00-report.md) and
|
||||||
|
[144](../../04-ISSUES/144-the-predecessors-rules-outlive-the-firewall-it-was-found-as/00-report.md).*
|
||||||
|
|
||||||
|
Retiring the found firewall was a step the flip took once, and said it had taken whatever happened;
|
||||||
|
on the first machine with one it did not take, and a hand's work fifty minutes later was recorded as
|
||||||
|
the mesh's. And "the firewall found" named one front end while a predecessor's chain in the container
|
||||||
|
runtime's user hook — legacy iptables on one machine, invisible to a reader of nftables — filtered the
|
||||||
|
forwarded path, refused ports the mesh declared open, and carried an allowance every module reaching
|
||||||
|
another by the machine's own name relied on.
|
||||||
|
|
||||||
|
**Convergence is a state the host keeps.** Every converged apply reads whether the found firewall is in
|
||||||
|
force; enabled again, it is retired again and said; the record says whether the mesh disabled it or
|
||||||
|
found it inactive, and a skipped step is said. **The host reports what filters the machine**, every
|
||||||
|
apply, adopted or converged: every table and legacy chain that refuses, with an owner — the mesh's,
|
||||||
|
the found firewall's, the runtime's own plumbing, a ban, or *other*, which is where the runtime's user
|
||||||
|
chain's refusals go. **The mesh says which:** `node show` lists them; `status` names a converged machine
|
||||||
|
anything *other* filters and is not well; the converge preview lists what filters the machine and the
|
||||||
|
fate of each — retired with the front end, left as the runtime's, left as a ban, or *left in force and
|
||||||
|
not the mesh's*. The mesh removes none of it; adoption's threshold does not move.
|
||||||
|
|
||||||
|
*How it is checked:* host tests over rulesets captured from three machines of this mesh classify every
|
||||||
|
refusing chain (a predecessor's chain in the legacy filter as *other*, a ban list reached through the
|
||||||
|
user chain as a ban, a leftover front-end chain as *other*); a fake front end enabled again on a
|
||||||
|
converged machine is retired again and said, found inactive is recorded as found; the report carries
|
||||||
|
the filters and the found firewall's state and a change in them is worth an unasked report; controller
|
||||||
|
tests over a fixture report check the recording, the preview's fates, the status JSON and the well
|
||||||
|
predicate. Live: the home server's record names the predecessor's chain as *other* and `status`
|
||||||
|
names the machine until the chain is removed by hand.
|
||||||
|
|
||||||
|
*2026-10-02, [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md):* removing what
|
||||||
|
the host reports as *other* is reached through the packet filter seat's `remove` verb, an operator's act
|
||||||
|
by name on the bus; the seat also serves `rules` and `reload`, and its holder's runtime declares the
|
||||||
|
`NET_ADMIN` capability on the machine's network. See design 33.
|
||||||
|
|
||||||
|
*2026-10-02, [ADR 0180](../../02-DECISIONS/0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md):*
|
||||||
|
once a machine is converged, the front end it was found with is uninstalled, not merely disabled — the
|
||||||
|
packet filter's holder declares its package absent after the mesh's filter is loaded, and a return to
|
||||||
|
adopted then enables nothing. The rollback path ADR 0100 kept on disk is given up on purpose.
|
||||||
|
|
||||||
## 5 — Certificates
|
## 5 — Certificates
|
||||||
|
|
||||||
**Two authorities, kept separate on purpose.**
|
**Two authorities, kept separate on purpose.**
|
||||||
@@ -842,6 +911,18 @@ One value, three readers:
|
|||||||
| `public` | the machine port, to anywhere | the public name | the public authority |
|
| `public` | the machine port, to anywhere | the public name | the public authority |
|
||||||
| `both` | the machine port, to anywhere | both names | each name's own authority |
|
| `both` | the machine port, to anywhere | both names | each name's own authority |
|
||||||
|
|
||||||
|
*2026-10-02.* **The proxy serves an internal name to the private network only**
|
||||||
|
([ADR 0138](../../02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md),
|
||||||
|
its insight of this date). It answers public and internal names on the same listeners, so the name a
|
||||||
|
request carries is the request's own claim, not where the request came from. An internal name is
|
||||||
|
served to the machines of the mesh and to the machine itself; to anyone else it is answered as a name
|
||||||
|
never routed, in the handshake as well as the request. Without this, an endpoint with reach `internal`
|
||||||
|
would be public under a name that is easy to guess
|
||||||
|
([issue 191](../../04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/00-report.md)).
|
||||||
|
The proxy is told who the mesh is, and its routes, in its membership on the bus — the same machine
|
||||||
|
addresses the filter's "from the mesh" is rendered from
|
||||||
|
([ADR 0167](../../02-DECISIONS/0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md)).
|
||||||
|
|
||||||
**An endpoint that is not routed is reached and never named.** No route contribution means no name is
|
**An endpoint that is not routed is reached and never named.** No route contribution means no name is
|
||||||
composed and no certificate requested, while the filter still acts on it. That is the case the model
|
composed and no certificate requested, while the filter still acts on it. That is the case the model
|
||||||
could not express at all, and it is the ordinary case for anything that is not HTTP.
|
could not express at all, and it is the ordinary case for anything that is not HTTP.
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ code:
|
|||||||
- mesh-host packaging/nox-mesh-host-network.sh
|
- mesh-host packaging/nox-mesh-host-network.sh
|
||||||
- mesh-controller internal/token
|
- mesh-controller internal/token
|
||||||
- mesh-controller internal/inventory/nodes.go
|
- mesh-controller internal/inventory/nodes.go
|
||||||
updated: 2026-09-23
|
updated: 2026-10-02
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md
|
||||||
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
||||||
- 02-DECISIONS/0004-a-node-and-how-it-joins.md
|
- 02-DECISIONS/0004-a-node-and-how-it-joins.md
|
||||||
- 02-DECISIONS/0005-the-node-host.md
|
- 02-DECISIONS/0005-the-node-host.md
|
||||||
@@ -311,6 +312,32 @@ found firewall again and converges the openings through it; what was taken stays
|
|||||||
a predecessor leaves one and asserts nothing that serves changes until a module is taken or the
|
a predecessor leaves one and asserts nothing that serves changes until a module is taken or the
|
||||||
node is converged, and that the flip closes exactly what the preview said.
|
node is converged, and that the flip closes exactly what the preview said.
|
||||||
|
|
||||||
|
**Taking a module is previewed, and the preview is a comparison** — revision, 2026-10-01
|
||||||
|
([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md)). For every held thing a
|
||||||
|
module would replace, `take` puts what runs beside what the module declares: a container's image and
|
||||||
|
its age, name, networks and their other members, published ports and their reach, mounts; a file's
|
||||||
|
kept original against the declared content, as a difference; a secret the mesh minted for a service
|
||||||
|
that already has one; the module's settings composed against its definition. An older image, a
|
||||||
|
differing file and a minted secret for found data refuse unless named; a narrowed port and a shared
|
||||||
|
network are said. `take --yes <digest>` cuts over what was previewed, as the flip does. A taken
|
||||||
|
container may keep a found network by a per-machine setting while its neighbours are not yet taken.
|
||||||
|
*How it is checked:* ADR 0163's table.
|
||||||
|
|
||||||
|
**A setting is judged where it is stored, and the take's words** — revision, 2026-10-02
|
||||||
|
([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1, 2, 4 and 6).
|
||||||
|
The preview ends with a digest of what it said; `take --yes <digest>` acts on that preview and nothing
|
||||||
|
else, and a preview that has changed since, or an account of the machine older than the flip allows, is
|
||||||
|
refused as the flip's is. A module the machine holds nothing for has nothing to compare, and `--yes`
|
||||||
|
suffices. The overrides are `--downgrade`, `--replace <path>` and `--mint <name>`; the per-machine
|
||||||
|
setting that keeps a found network is `networks`, a container id to the networks it keeps, accepted
|
||||||
|
for an adopted machine only. Storing a setting composes it against the module's current definition and
|
||||||
|
refuses, naming node, module, layer and key, what cannot compose or reaches nothing. A definition that
|
||||||
|
later moves under a stored setting costs that module its place in the machine's declaration, said by
|
||||||
|
name in `plan`, `push` and the declaration itself, and the machine is told everything else; a stray
|
||||||
|
setting no longer refuses the machine where it is read. *How it is checked:* controller tests over the
|
||||||
|
one judgement — refused where stored, a module left out where composed, the envelope naming it — and
|
||||||
|
over a take's digest, staleness and secrets.
|
||||||
|
|
||||||
A candidate machine is not empty. It has a package manager, probably a container runtime,
|
A candidate machine is not empty. It has a package manager, probably a container runtime,
|
||||||
configuration somebody chose. [ADR 0005](../../02-DECISIONS/0005-the-node-host.md)
|
configuration somebody chose. [ADR 0005](../../02-DECISIONS/0005-the-node-host.md)
|
||||||
says the host never touches what it did not create — adoption is the deliberate act of taking
|
says the host never touches what it did not create — adoption is the deliberate act of taking
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ code:
|
|||||||
- mesh-controller internal/inventory/secrets.go
|
- mesh-controller internal/inventory/secrets.go
|
||||||
- mesh-controller cmd/mesh-controller/rotate.go
|
- mesh-controller cmd/mesh-controller/rotate.go
|
||||||
- mesh-controller examples/postgres-provisioner
|
- mesh-controller examples/postgres-provisioner
|
||||||
updated: 2026-09-21
|
updated: 2026-10-01
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md
|
- 02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md
|
||||||
- 02-DECISIONS/0009-modules-and-the-graph.md
|
- 02-DECISIONS/0009-modules-and-the-graph.md
|
||||||
@@ -136,3 +136,16 @@ rotates, and is queried for who holds it, through exactly the machinery describe
|
|||||||
The rotation a module's own secret lacks is not a second mechanism; it is this one, pointed at a
|
The rotation a module's own secret lacks is not a second mechanism; it is this one, pointed at a
|
||||||
secret the vault provides. What this page proves for a database password holds, by construction,
|
secret the vault provides. What this page proves for a database password holds, by construction,
|
||||||
for a secret from the vault.
|
for a secret from the vault.
|
||||||
|
|
||||||
|
*Built 2026-10-01, the read-at-start half ([issue 180](../../04-ISSUES/180-a-modules-own-secret-cannot-be-rotated/00-report.md),
|
||||||
|
[ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md)).* An own secret
|
||||||
|
says how the module takes it — `taken: at-start` or `taken: applied` on its entry — and the mesh
|
||||||
|
rotates only the first: `secret rotate <node> <module> <name>` makes it anew, seals it to the machine
|
||||||
|
and the operator, and sends the machine, so the module starts again on it. A secret that says neither
|
||||||
|
is refused with the word to write, because a credential rotated under software that never reads it
|
||||||
|
again is the fault of issue 179 made deliberately; an applied one is refused until the staged form is
|
||||||
|
built; an accepted one is refused as ADR 0113 says. `rotate` is a verb on the controller's seat with
|
||||||
|
both shapes, so the console asks for either. A provider that shares its one credential with every
|
||||||
|
consumer ([ADR 0158](../../02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md))
|
||||||
|
rotates the same way, with every holder's copy remade and every holding machine sent together. *How it is checked:* the tests named in issue 180, and a
|
||||||
|
live rotation through the console of a secret a module reads at start.
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ code:
|
|||||||
- mesh-controller cmd/mesh-builder
|
- mesh-controller cmd/mesh-builder
|
||||||
- mesh-controller internal/builder
|
- mesh-controller internal/builder
|
||||||
- mesh-catalog modules/builder
|
- mesh-catalog modules/builder
|
||||||
updated: 2026-09-30
|
updated: 2026-10-01
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||||
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
||||||
- 02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md
|
- 02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md
|
||||||
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
|
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
|
||||||
@@ -186,7 +187,8 @@ disagrees with it.
|
|||||||
| `grants` | credentials it must create for its consumers |
|
| `grants` | credentials it must create for its consumers |
|
||||||
| `filtering` | rules beyond its own ports |
|
| `filtering` | rules beyond its own ports |
|
||||||
| `computed` | marks a module the controller generates rather than an author writing |
|
| `computed` | marks a module the controller generates rather than an author writing |
|
||||||
| `build.artifacts` | what it produces |
|
| `build.artifacts` | what it produces; an artifact's `context` may be a URL or a path on the git seat (`seat: git`), composed by the mesh that builds it |
|
||||||
|
| `names-on-purpose` | on a resource: each name it means to name — the world's federation server, a registry that built an application the mesh does not — with its reason. A definition names no installation, and a test says so ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)) |
|
||||||
|
|
||||||
**A container mounts only what the manifest declares**
|
**A container mounts only what the manifest declares**
|
||||||
([ADR 0091](../../02-DECISIONS/0091-a-mount-is-declared-three-ways.md)). A bind mount the module
|
([ADR 0091](../../02-DECISIONS/0091-a-mount-is-declared-three-ways.md)). A bind mount the module
|
||||||
@@ -233,10 +235,10 @@ counts as a copy and what as a base.
|
|||||||
|
|
||||||
| resource | is | a module may |
|
| resource | is | a module may |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `directory` | a directory with a mode and an owner | ✅ |
|
| `directory` | a directory with a mode and an owner, **placed by the mesh** under the node's root: `place: "."` is the assignment's own root, `place: "mesh"` the mesh's directory for the module, a pathless one sits beneath the root by its id; a stated path is the placement for data that must stay where it is, and may itself sit beneath a placed one (`${dir:<id>}/…`). Everything else names it as `${dir:<id>}` ([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md), [174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)) | ✅ |
|
||||||
| `file` | literal content, with `${bound:…}` and `${secret:…}` filled in | ✅ |
|
| `file` | literal content, with `${bound:…}`, `${secret:…}`, `${dir:…}`, `${port:…}`, `${machine:…}` and `${setting:…}` filled in — the last an operator's value from the assignment's settings, refused by name when unset ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)) | ✅ |
|
||||||
| `user` | a login | ✅ |
|
| `user` | a login | ✅ |
|
||||||
| `access` | a pre-existing path it may use and must not own | ✅ |
|
| `access` | a pre-existing path it may use and must not own, **named by id** and placed by the assignment (`accesses: {<id>: <path>}` on its settings); mounts say `${access:<id>}`; a path in the definition is the default an assignment replaces, tolerated while the catalogue converts ([issue 153](../../04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)) | ✅ |
|
||||||
| `archive` | files fetched by digest and unpacked | ✅ |
|
| `archive` | files fetched by digest and unpacked | ✅ |
|
||||||
| `package` | a package that must be present | ✅ |
|
| `package` | a package that must be present | ✅ |
|
||||||
| `network` | a named container network | ✅ |
|
| `network` | a named container network | ✅ |
|
||||||
@@ -266,6 +268,31 @@ ships one and wrong for code the mesh built, which has no unit until the mesh wr
|
|||||||
**Tools, hooks and consumers are not further modes**, which is the test of whether three is the
|
**Tools, hooks and consumers are not further modes**, which is the test of whether three is the
|
||||||
right number: they are loaded by a tool host, and a tool host is a process that stays up.
|
right number: they are loaded by a tool host, and a tool host is a process that stays up.
|
||||||
|
|
||||||
|
## A build says what it does, as it happens
|
||||||
|
|
||||||
|
*2026-10-01 — [ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md).*
|
||||||
|
|
||||||
|
A build machine narrates every build on the bus as the role it holds: `started` when it takes the
|
||||||
|
work, one `log.<build id>` event per line — every command it runs with its duration, every step of
|
||||||
|
the recipe, and on failure the command's own output, line by line — and `built` for the outcome as
|
||||||
|
before. The same lines still go to the machine's standard error, so a build machine with nobody
|
||||||
|
listening is as readable as it was; a listener reads the same lines, live, from anywhere on the mesh.
|
||||||
|
|
||||||
|
One build is one subject. A reader follows it by subscribing that subject and nothing else, and the
|
||||||
|
events stream keeps it for a week, so `builds --log <id>` — on the command line and as the
|
||||||
|
controller's seat verb through the console — reads it back afterwards. `builds` lists every build's
|
||||||
|
id beside it, and `build` says the id it asked with. The mesh keeps no second copy: the stream is the
|
||||||
|
log. The console's `build` tool asks and answers at once with the id; the outcome is taken in — the
|
||||||
|
build recorded, the module registered with its source — by whoever hears it, the waiting command or
|
||||||
|
the daemon following the role's event, so a build nobody waited for still reaches the catalogue
|
||||||
|
([issue 176](../../04-ISSUES/176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md)). A viewer of builds, when one is built, is a subscriber over these subjects and the outcome; the
|
||||||
|
builder needs nothing more for it.
|
||||||
|
|
||||||
|
*How it is checked:* the holder's grant is exactly `started`, `built` and `log.*` (broker test); a
|
||||||
|
build's lines reach a reader of its subject in order and the stream holds them afterwards (link test
|
||||||
|
against a real server); the seat verb with an id reads the log (controller test); and, live, a build
|
||||||
|
after the roll-out read line by line through the console.
|
||||||
|
|
||||||
## The builder compiles the languages the mesh is written in
|
## The builder compiles the languages the mesh is written in
|
||||||
|
|
||||||
*2026-09-29 —
|
*2026-09-29 —
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: implemented
|
status: implemented
|
||||||
code: [mesh-catalog, mesh-controller, mesh-host]
|
code: [mesh-catalog, mesh-controller, mesh-host]
|
||||||
updated: 2026-09-21
|
updated: 2026-10-01
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md
|
||||||
- 02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md
|
- 02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md
|
||||||
- 02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md
|
- 02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md
|
||||||
- 02-DECISIONS/0085-a-secret-is-a-provision.md
|
- 02-DECISIONS/0085-a-secret-is-a-provision.md
|
||||||
@@ -127,6 +128,22 @@ credential a provider grants; the export names each entry by the node and module
|
|||||||
the name they know it by, and says whether it is a module's own secret or a pair credential, so
|
the name they know it by, and says whether it is a module's own secret or a pair credential, so
|
||||||
recovery addresses both alike.
|
recovery addresses both alike.
|
||||||
|
|
||||||
|
### A provider with one credential
|
||||||
|
|
||||||
|
*Decided 2026-10-01 ([ADR 0158](../../02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md)); the controller's half built the same day (mesh-controller PR 184): the offer's word, the need carrying the shared secret's name, the vault's one value under one generation stamp, remade for every holder on a later binding or a rotation, the rotate command sending every holder. What remains is each provider's definition saying `credential` and `taken`, with a start that applies the file — the media catalogue's work.*
|
||||||
|
|
||||||
|
Software that holds one credential — a download client's web password, an indexer's one API key —
|
||||||
|
cannot give each consumer a login, so ADR 0048's form does not fit it and its values were accepted
|
||||||
|
by hand. An offer may now say `"credential": {"own": "<secret>"}`: the provider's own secret *is* the
|
||||||
|
credential every consumer of that provision receives, in the shape of an ordinary pair credential,
|
||||||
|
under the provider's one user name. The vault keeps one value per provider assignment and provision,
|
||||||
|
sealed to the provider's machine, each consumer's machine and the operator; because it holds no
|
||||||
|
plaintext it remakes the value for every holder at once when a consumer binds or unbinds or a
|
||||||
|
rotation is asked, and the mesh sends every holding machine together. The provider takes it as it
|
||||||
|
says it takes its own secret (`taken`, issue 180); consumers read it at start. An accepted value is
|
||||||
|
sealed to the consumers of the moment and not remade; a consumer that binds later waits for the next
|
||||||
|
acceptance. *How it is checked:* the rows of ADR 0158's table; the controller's rows pass, the live row waits for the first provider.
|
||||||
|
|
||||||
## Beyond generate and hold
|
## Beyond generate and hold
|
||||||
|
|
||||||
Owning a secret means owning more than its creation. The mesh being migrated onto has a working
|
Owning a secret means owning more than its creation. The mesh being migrated onto has a working
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ code:
|
|||||||
- mesh-tools src/broker-amqp.ts (to be replaced)
|
- mesh-tools src/broker-amqp.ts (to be replaced)
|
||||||
- mesh-catalog modules/nats (to be written)
|
- mesh-catalog modules/nats (to be written)
|
||||||
- mesh-sdk src (the protocol's NATS binding, step 3)
|
- mesh-sdk src (the protocol's NATS binding, step 3)
|
||||||
updated: 2026-09-27
|
updated: 2026-10-02
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md
|
||||||
|
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||||
|
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||||
- 02-DECISIONS/0106-the-bus-is-nats.md
|
- 02-DECISIONS/0106-the-bus-is-nats.md
|
||||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||||
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
||||||
@@ -80,8 +83,24 @@ mesh.seat.<seat>.accept.<verb> work submitted to a role (JetStream: per-s
|
|||||||
mesh.seat.<seat>.event.<verb> a role's own event (JetStream: EVENTS)
|
mesh.seat.<seat>.event.<verb> a role's own event (JetStream: EVENTS)
|
||||||
mesh.seat.<seat>.tool.<verb> a role's tool (core request/reply)
|
mesh.seat.<seat>.tool.<verb> a role's tool (core request/reply)
|
||||||
mesh.ask.<node>.<command> the controller's command api (core request/reply)
|
mesh.ask.<node>.<command> the controller's command api (core request/reply)
|
||||||
|
mesh.assignment.<node>.<module> an assignment's membership (JetStream: ASSIGNMENTS, last-per-subject)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Revised 2026-10-01** ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)): the rows above
|
||||||
|
for a module's and a seat's tools are the shapes the controller *issues*, not rules a runtime carries.
|
||||||
|
Every assignment is published a membership — what it serves and where, in which queue, its seat verbs,
|
||||||
|
where its events land, what it may reach — on `mesh.assignment.<node>.<module>`, kept last per subject
|
||||||
|
like a declaration, republished when the assignment's facts change. The runtime serves exactly that
|
||||||
|
list; the account's grant is the same membership read the other way; the console's listing carries each
|
||||||
|
tool's subject. The one rule a runtime keeps is the membership's own subject, from the two names in its
|
||||||
|
credential.
|
||||||
|
|
||||||
|
**Revised 2026-10-02** ([ADR 0167](../../02-DECISIONS/0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md)):
|
||||||
|
a membership also carries **what its module receives**, by requirement — the contributions its
|
||||||
|
received file is written from, from the same composition — and **who the mesh is**, every machine's
|
||||||
|
address on the private network, the list the filter's "from the mesh" is rendered from. The route
|
||||||
|
proxy is the first reader of both.
|
||||||
|
|
||||||
**Revised 2026-09-27** ([ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md)):
|
**Revised 2026-09-27** ([ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md)):
|
||||||
**`mesh.build.request`, `mesh.control.built` and the BUILDS stream are gone.** A build is work submitted to a role, and the
|
**`mesh.build.request`, `mesh.control.built` and the BUILDS stream are gone.** A build is work submitted to a role, and the
|
||||||
mesh already has a shape for that — a seat's `accept` subjects, on a work queue with a queue group of
|
mesh already has a shape for that — a seat's `accept` subjects, on a work queue with a queue group of
|
||||||
@@ -135,7 +154,7 @@ Core NATS is at-most-once. Everything the mesh must not lose lives in a JetStrea
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| CONTROL | `mesh.control.>` except `alive` (a build's outcome moved to its seat, ADR 0121) | work queue, one consumer (the controller), explicit ack | the store-window guarantee ([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)): the controller `nak`s with a delay while its store is away and the message is redelivered; nothing is dropped |
|
| CONTROL | `mesh.control.>` except `alive` (a build's outcome moved to its seat, ADR 0121) | work queue, one consumer (the controller), explicit ack | the store-window guarantee ([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)): the controller `nak`s with a delay while its store is away and the message is redelivered; nothing is dropped |
|
||||||
| NODES | `mesh.node.>` | last per subject | one declaration per node, always the newest |
|
| NODES | `mesh.node.>` | last per subject | one declaration per node, always the newest |
|
||||||
| EVENTS | `mesh.mod.*.event.>` | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream) |
|
| EVENTS | `mesh.mod.*.event.>` and `mesh.seat.*.event.>` | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream). *2026-10-01:* a build's whole log is here too, as the build-machine seat's `log.<build id>` events ([ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)) — one subject per build, a week of retention, read back by `builds --log <id>` with a consumer that is gone when the reading is done |
|
||||||
|
|
||||||
Tool calls and heartbeats stay on core NATS: a lost heartbeat is the next heartbeat; a lost tool
|
Tool calls and heartbeats stay on core NATS: a lost heartbeat is the next heartbeat; a lost tool
|
||||||
call is a timeout the caller already handles.
|
call is a timeout the caller already handles.
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ code:
|
|||||||
- mesh-controller cmd/mesh-controller/source.go
|
- mesh-controller cmd/mesh-controller/source.go
|
||||||
- mesh-controller internal/inventory/migrations/0032-a-source-may-live-on-a-seat.sql
|
- mesh-controller internal/inventory/migrations/0032-a-source-may-live-on-a-seat.sql
|
||||||
- mesh-catalog modules/gitea/module.json
|
- mesh-catalog modules/gitea/module.json
|
||||||
updated: 2026-09-27
|
updated: 2026-10-01
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0161-what-deserves-a-seat.md
|
||||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||||
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
||||||
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
|
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
|
||||||
@@ -75,6 +76,17 @@ named at the wrong scope. Adding a seat is a decision, recorded, for the reason
|
|||||||
host's vocabulary is one: the set is what a person reads to learn what a mesh can have, and an entry
|
host's vocabulary is one: the set is what a person reads to learn what a mesh can have, and an entry
|
||||||
nobody argued for is an entry nobody can explain.
|
nobody argued for is an entry nobody can explain.
|
||||||
|
|
||||||
|
**What deserves one** ([ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md)). A provision the
|
||||||
|
mesh's own code dereferences by name is delivered by a mesh seat its provider claims — the store, the
|
||||||
|
bus, the vault, the artifact store, the catalogue. A provision a module merely offers may have several
|
||||||
|
providers, and a consumer with several is a person's choice. A fact of the shape *exactly one machine
|
||||||
|
is X* — the hub — is not a seat, because a seat is held by a module assignment and points at it; it is
|
||||||
|
a placement with a capacity of one, kept by the store, refused by name when a second is placed, and
|
||||||
|
named in the listing. And a seat whose role is to speak to what the machine runs — the uplink — is
|
||||||
|
held only by the dialect the machine runs: the machine says which in its profile, renewed with every
|
||||||
|
report, and the holder declares the capability, so the wrong one is refused the way any missing
|
||||||
|
capability is.
|
||||||
|
|
||||||
## The set
|
## The set
|
||||||
|
|
||||||
**The set is derived, and only the mesh's half is written here.** Revision, 2026-09-26
|
**The set is derived, and only the mesh's half is written here.** Revision, 2026-09-26
|
||||||
@@ -108,8 +120,8 @@ convention, which later seats departed from.
|
|||||||
| `mesh-controller` | — | mesh | — | the controller |
|
| `mesh-controller` | — | mesh | — | the controller |
|
||||||
| `mesh-store` | — | mesh | — | the store the mesh's own records live in |
|
| `mesh-store` | — | mesh | — | the store the mesh's own records live in |
|
||||||
| `mesh-broker` | — | mesh | `mesh-bus` | the broker carrying the mesh's own bus |
|
| `mesh-broker` | — | mesh | `mesh-bus` | the broker carrying the mesh's own bus |
|
||||||
| `mesh-vault` | — | mesh | `secret`, reserved | the vault |
|
| `mesh-vault` | — | mesh | `secret` | the vault (in the seed since [ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md); the earlier *reserved* named an effect no rule produced) |
|
||||||
| `mesh-artifact-store` | `the-artifact-store` | mesh | `artifact-store` | the artifact registry |
|
| `mesh-artifact-store` | `the-artifact-store` (renamed 2026-09-30, [ADR 0156](../../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)) | mesh | `artifact-store` | the artifact registry |
|
||||||
| `mesh-catalog` | `the-catalogue` | mesh | — | the catalogue |
|
| `mesh-catalog` | `the-catalogue` | mesh | — | the catalogue |
|
||||||
| `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge |
|
| `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge |
|
||||||
| `mesh-git` | `git` | mesh | `git` | the forge |
|
| `mesh-git` | `git` | mesh | `git` | the forge |
|
||||||
@@ -240,6 +252,8 @@ checked as their tables say:
|
|||||||
| A handover replaces the holder as one write, needs an assignment to point at, and goes with it | 0131: store tests — a second handover leaves one row; a handover to a module not assigned where named is refused; unassigning the holder removes the row. |
|
| A handover replaces the holder as one write, needs an assignment to point at, and goes with it | 0131: store tests — a second handover leaves one row; a handover to a module not assigned where named is refused; unassigning the holder removes the row. |
|
||||||
| A requirement naming a seat is answered by its holder; a foundation seat cannot be named | 0118: resolution tests with a second provider on the consumer's node, with the seat unheld, and naming `mesh-store`. |
|
| A requirement naming a seat is answered by its holder; a foundation seat cannot be named | 0118: resolution tests with a second provider on the consumer's node, with the seat unheld, and naming `mesh-store`. |
|
||||||
| Several providers and none local is a person's choice | 0118: an assignment test listing candidates with the seat's holder first and recording the pin. |
|
| Several providers and none local is a person's choice | 0118: an assignment test listing candidates with the seat's holder first and recording the pin. |
|
||||||
| `secret` is reserved | 0118: the parser and resolution refusals for another provider and a pin. |
|
| `secret` has one provider, the holder of `mesh-vault` | 0161: a second claimant of the seat is refused by name (`CanHold`); *correction of fact, 2026-10-01: no parser rule ever reserved the word, the seat does the work*. |
|
||||||
|
| A singular fact about machines is a placement of capacity one, refused by name | 0161: the overlay command's test for a second hub; the store's unique index. |
|
||||||
|
| A holder of `node-uplink` is the dialect the machine runs | 0161: the host reports `uplink-<manager>` in its profile with every report; a resolution test refuses the other holder naming the capability. |
|
||||||
| Holdings are derived, and the overview lists every seat | 0118: the `seats` command test, including an unheld seat. |
|
| Holdings are derived, and the overview lists every seat | 0118: the `seats` command test, including an unheld seat. |
|
||||||
| A build source on the seat records no address; an unheld seat refuses only self-hosted builds | 0111's tests. |
|
| A build source on the seat records no address; an unheld seat refuses only self-hosted builds | 0111's tests. |
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: proposed
|
status: in-progress
|
||||||
code: []
|
code: [mesh-controller internal/catalogue]
|
||||||
updated: 2026-09-26
|
updated: 2026-09-30
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
- 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
|
||||||
- 02-DECISIONS/0115-one-assignment-of-a-module-per-node.md
|
- 02-DECISIONS/0115-one-assignment-of-a-module-per-node.md
|
||||||
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
|
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
|
||||||
- 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md
|
- 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md
|
||||||
@@ -136,6 +137,12 @@ lost is a named volume, not a directory ([ADR 0030](../../02-DECISIONS/0030-data
|
|||||||
the assignment says nothing;
|
the assignment says nothing;
|
||||||
- **a placement**, where the assignment puts one directory elsewhere: on a second disk, or where an
|
- **a placement**, where the assignment puts one directory elsewhere: on a second disk, or where an
|
||||||
adopted machine's data already is ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
|
adopted machine's data already is ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
|
||||||
|
*Built 2026-10-01 ([issue 153](../../04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)):*
|
||||||
|
`places` on the assignment's settings, by directory id, with an owner where the data already has
|
||||||
|
one; and `accesses`, by access id, for the operator's data — an access has an id and its mounts
|
||||||
|
name it as `${access:<id>}`. Both validated as `endpoints` is: an id the definition does not
|
||||||
|
declare is refused. *How it is checked:* the controller's placement tests, and the
|
||||||
|
path-preservation proof extended to accesses.
|
||||||
|
|
||||||
**An operator's shared data** is an access, as before ([ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md)):
|
**An operator's shared data** is an access, as before ([ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md)):
|
||||||
never created, owned or removed by the mesh. The module requires read or read-write access. Where
|
never created, owned or removed by the mesh. The module requires read or read-write access. Where
|
||||||
@@ -171,6 +178,34 @@ make a new external key, so rotating one means an operator handing over a new va
|
|||||||
assignment, and the route provider answers. A public name already held by another assignment is
|
assignment, and the route provider answers. A public name already held by another assignment is
|
||||||
refused, like any other singular thing.
|
refused, like any other singular thing.
|
||||||
|
|
||||||
|
**Built so far, 2026-09-30** ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)):
|
||||||
|
the operator's value in its first form — `${setting:<key>}` in a file's content, from the assignment's
|
||||||
|
settings layers, refused by name when nothing set it; a module told the name its route composes
|
||||||
|
(`${bound:<route>:name}`); a build context on the git seat; and the check that no definition names an
|
||||||
|
installation, with `names-on-purpose` for the names a definition means. The host's directory in its
|
||||||
|
first form is [`${dir:<id>}`](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md),
|
||||||
|
a placed directory under the node's root. Each is this design's provider in the shape the existing
|
||||||
|
placeholders have, not yet the one requirement form below; they are phase 1's first cases.
|
||||||
|
|
||||||
|
*Phase 3, in part (2026-09-30):* every definition's **own** data directory is placed; the conversion
|
||||||
|
moved no data, proven by resolving both catalogues with the controller's rule and comparing
|
||||||
|
([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md)).
|
||||||
|
What the mesh writes *for* a module was still placed by the definition
|
||||||
|
([issue 174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)),
|
||||||
|
the gap this design answered on 2026-09-26; built later the same day: a directory saying
|
||||||
|
`place: "mesh"` is `<root>/mesh/<module>`, a directory beneath a placed one states its path as
|
||||||
|
`${dir:<id>}/<rest>` and moves with it, and the same proof — both catalogues resolved and compared —
|
||||||
|
shows forty-eight definitions naming the paths they named before.
|
||||||
|
|
||||||
|
*The operator's value travels only where it is asked for (2026-09-30,
|
||||||
|
[issue 173](../../04-ISSUES/173-a-modules-settings-reach-every-fact-it-contributes/00-report.md)):*
|
||||||
|
a setting overrides a key a contribution or a served fact declares and adds none; a file keeps taking
|
||||||
|
any key. A provider that must tell its consumers an operator's value — a mail server's domain, an
|
||||||
|
identity provider's issuer — declares it in what it serves as `${setting:<key>}`, and it is refused by
|
||||||
|
name when nothing sets it. That is the contract half of this design's operator provider in the shape
|
||||||
|
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.
|
||||||
|
|
||||||
## How a definition reads what was resolved
|
## 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
|
**One form, naming a requirement and a field of its contract.** A definition that needs the database's
|
||||||
@@ -379,7 +414,9 @@ writes (`/var/lib/mesh/<module>`: sealed credentials, composed bindings) need a
|
|||||||
module-visible reservation. They do not: a module *requires* a `host-path` and receives a
|
module-visible reservation. They do not: a module *requires* a `host-path` and receives a
|
||||||
location; what the mesh writes for the module is the mesh's plumbing, placed where the mesh
|
location; what the mesh writes for the module is the mesh's plumbing, placed where the mesh
|
||||||
chooses and mounted in — never part of the module's contract. One reservation per
|
chooses and mounted in — never part of the module's contract. One reservation per
|
||||||
requirement, `<root>/<module>/<name>`.
|
requirement, `<root>/<module>/<name>`. *(Built 2026-09-30: the mesh's directory for a module is
|
||||||
|
`<root>/mesh/<module>`, named in the definition as a placed directory and nowhere as a path —
|
||||||
|
issue 174.)*
|
||||||
|
|
||||||
**Resolution happens in the controller, at declaration composition.** The node receives
|
**Resolution happens in the controller, at declaration composition.** The node receives
|
||||||
concrete paths exactly as today — the wire format and the host's apply do not change for
|
concrete paths exactly as today — the wire format and the host's apply do not change for
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: proposed
|
status: in-progress
|
||||||
code: []
|
code:
|
||||||
updated: 2026-09-27
|
- mesh-controller internal/inventory
|
||||||
|
- mesh-controller internal/catalogue
|
||||||
|
- mesh-controller cmd/mesh-controller
|
||||||
|
updated: 2026-10-02
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||||
- 02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md
|
- 02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md
|
||||||
@@ -14,10 +17,10 @@ decisions:
|
|||||||
# 29 — A node has operator accounts, and the mesh owns what lives under a home
|
# 29 — A node has operator accounts, and the mesh owns what lives under a home
|
||||||
|
|
||||||
**The mesh models machines but not the people on them.** A node record holds its name, its
|
**The mesh models machines but not the people on them.** A node record holds its name, its
|
||||||
address, its mode — and nothing about *who a person is* on it: `jochens` on novox, `ace` on ace,
|
address, its mode — and nothing about *who a person is* on it: one login name on the build node,
|
||||||
`jochen` on shanks and g14. That username is not incidental. It decides who a file under `~` is
|
another on the home-server, a third on both workstations. That username is not incidental. It
|
||||||
owned by, who a user service runs as, and — the case that surfaced this — which account `ssh
|
decides who a file under `~` is owned by, who a user service runs as, and — the case that surfaced
|
||||||
<node>` logs in as. The predecessor knew it (its per-node `user:`, and the modules that wrote a
|
this — which account `ssh <node>` logs in as. The predecessor knew it (its per-node `user:`, and the modules that wrote a
|
||||||
person's `~/.ssh/config`, `~/.zshrc`, `~/.config`); the mesh, taking those over, kept the machine
|
person's `~/.ssh/config`, `~/.zshrc`, `~/.config`); the mesh, taking those over, kept the machine
|
||||||
facts and dropped the human one.
|
facts and dropped the human one.
|
||||||
|
|
||||||
@@ -28,8 +31,9 @@ Several things are missing, and they are one idea.
|
|||||||
A node has one or more **operator accounts**: the human logins on it. At minimum a name; the
|
A node has one or more **operator accounts**: the human logins on it. At minimum a name; the
|
||||||
mesh already knows the node and its address, so `<account>@<node>` is then a complete answer to
|
mesh already knows the node and its address, so `<account>@<node>` is then a complete answer to
|
||||||
"who am I, where." It is the mesh's to hold because everything below is derived from it, and
|
"who am I, where." It is the mesh's to hold because everything below is derived from it, and
|
||||||
because it is exactly the fact that was silently lost — `ssh ace` failed to `ace` because nothing
|
because it is exactly the fact that was silently lost — `ssh home-server` logged in under the
|
||||||
in the mesh said ace's account is `ace`.
|
workstation's own name, because nothing in the mesh said the home-server's account is a different
|
||||||
|
one.
|
||||||
|
|
||||||
## 2. A resource may live under a home, owned by its account
|
## 2. A resource may live under a home, owned by its account
|
||||||
|
|
||||||
@@ -65,14 +69,14 @@ create `~/.ssh` at `0700`, chown it to the account, and own the files it places
|
|||||||
|
|
||||||
**The boundary — and it is the reason this is safe:** `~/.ssh` is the one directory where a wrong
|
**The boundary — and it is the reason this is safe:** `~/.ssh` is the one directory where a wrong
|
||||||
declaration locks a person out of their own machine. So the mesh's *found-vs-owned* semantics
|
declaration locks a person out of their own machine. So the mesh's *found-vs-owned* semantics
|
||||||
([ADR 0126](../../02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md),
|
([ADR 0118](../../02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md),
|
||||||
adoption) apply *inside* the home directory. The mesh **owns** the directory and the files above; it
|
adoption) apply *inside* the home directory. The mesh **owns** the directory and the files above; it
|
||||||
**holds as found — never rewrites, never removes** — the operator's own contents: their **private
|
**holds as found — never rewrites, never removes** — the operator's own contents: their **private
|
||||||
keys** and their **personal drop-ins** (`config.d/personal`, the personal `Host` aliases a
|
keys** and their **personal drop-ins** (`config.d/personal`, the personal `Host` aliases a
|
||||||
workstation carries, exactly as `hosts.local` is the home the mesh never rewrites for `/etc/hosts`).
|
workstation carries, exactly as `hosts.local` is the home the mesh never rewrites for `/etc/hosts`).
|
||||||
Reconcile removing an unassigned `config.d/mesh` is fine; the same logic aimed at `id_ed25519` or an
|
Reconcile removing an unassigned `config.d/mesh` is fine; the same logic aimed at `id_ed25519` or an
|
||||||
operator's own `authorized_keys` entry is a lockout. This is the login-channel cousin of the rule
|
operator's own `authorized_keys` entry is a lockout. This is the login-channel cousin of the rule
|
||||||
[ADR 0125](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md) draws for the uplink and the sshd
|
[ADR 0117](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md) draws for the uplink and the sshd
|
||||||
module draws for the firewall: **the mesh must never be able to arrange the one failure that severs
|
module draws for the firewall: **the mesh must never be able to arrange the one failure that severs
|
||||||
its own way back in.** The carve-out is not a convenience; it is that rule, in `~/.ssh`.
|
its own way back in.** The carve-out is not a convenience; it is that rule, in `~/.ssh`.
|
||||||
|
|
||||||
@@ -108,12 +112,12 @@ found-vs-owned boundary of §3 is exactly what guarantees nothing already there
|
|||||||
|
|
||||||
None of this needs a node to discover the mesh, and none of it needs a control-plane module of its
|
None of this needs a node to discover the mesh, and none of it needs a control-plane module of its
|
||||||
own. The ssh files are **roster facts**
|
own. The ssh files are **roster facts**
|
||||||
([ADR 0128](../../02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md)): once the
|
([ADR 0120](../../02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md)): once the
|
||||||
roster view carries a node's **host key** and its **account** beside its name and address, the
|
roster view carries a node's **host key** and its **account** beside its name and address, the
|
||||||
`ssh-client` module ships a template for `known_hosts`, `config` and `authorized_keys`, and the
|
`ssh-client` module ships a template for `known_hosts`, `config` and `authorized_keys`, and the
|
||||||
controller renders each node's copy from the full roster and pushes it. The mesh owns the data; the
|
controller renders each node's copy from the full roster and pushes it. The mesh owns the data; the
|
||||||
module owns ssh's format; the control plane gains no ssh syntax. It is the same act as composing a
|
module owns ssh's format; the control plane gains no ssh syntax. It is the same act as composing a
|
||||||
peer list or `/etc/hosts` — which is why there is **no novox-only "mesh-ssh" module**: the
|
peer list or `/etc/hosts` — which is why there is **no control-node-only "mesh-ssh" module**: the
|
||||||
centralization is the controller's composition, not a module that runs somewhere. Only non-secret
|
centralization is the controller's composition, not a module that runs somewhere. Only non-secret
|
||||||
facts travel (names, addresses, accounts, host keys, the CA public key); the private key stays the
|
facts travel (names, addresses, accounts, host keys, the CA public key); the private key stays the
|
||||||
operator's, placed as an operator-owned file, referenced by path.
|
operator's, placed as an operator-owned file, referenced by path.
|
||||||
@@ -129,6 +133,44 @@ operator's, placed as an operator-owned file, referenced by path.
|
|||||||
They meet at the account and the CA, not at a bespoke module. The `sshd` server side already exists;
|
They meet at the account and the CA, not at a bespoke module. The `sshd` server side already exists;
|
||||||
the client/identity side and the CA are the open pieces.
|
the client/identity side and the CA are the open pieces.
|
||||||
|
|
||||||
|
## What has shipped, and what has not
|
||||||
|
|
||||||
|
*Recorded 2026-10-01 from the controller's main branch, not from intent.*
|
||||||
|
|
||||||
|
**Built (mesh-controller, merged 2026-09-27):**
|
||||||
|
|
||||||
|
- **§1, the account as a node fact.** A node record carries an operator account and, optionally,
|
||||||
|
its home. Empty is a real state — a freshly enrolled or headless machine has no operator account
|
||||||
|
known yet — and an empty home means *derive it* (the superuser's home for the superuser, the
|
||||||
|
conventional per-user home otherwise), so the common case needs no entry. The controller's node
|
||||||
|
command sets it. One account per node is what exists; "one or several" below is still open.
|
||||||
|
- **§2, resources under a home.** The account and its home are offered as machine facts, and a
|
||||||
|
resource's *path and owner* resolve placeholders exactly as its content does — so a module places
|
||||||
|
a file under a person's home, owned by that person, naming neither. A roster file may say it lives
|
||||||
|
under the home: it is rendered per node, placed under that node's account's home, chowned to the
|
||||||
|
account, and a node with no account gets none.
|
||||||
|
- **§5, the composed ssh config.** The roster rendering carries each node's account, so the
|
||||||
|
`ssh-client` template can emit a `Host` block per node with the right login name. Composed
|
||||||
|
end-to-end in the controller's tests.
|
||||||
|
|
||||||
|
**Written but not shipped:** the `ssh-client` catalogue module itself exists on a branch of the
|
||||||
|
module repository; its pull request was closed with a hold until this design is deployed, and
|
||||||
|
nothing has deployed it since. The predecessor's generator still writes every workstation's ssh
|
||||||
|
client blocks today — which is where [issue 172](../../04-ISSUES/172-the-ssh-client-block-matches-one-spelling-of-a-machine/00-report.md)
|
||||||
|
was found.
|
||||||
|
|
||||||
|
**Not built:** the SSH CA and certificates (§4), `known_hosts` and `authorized_keys` as roster files,
|
||||||
|
the found-vs-owned boundary inside `~/.ssh` (§3 — the controller has no rule yet that refuses to
|
||||||
|
rewrite a private key), adoption of existing keys, the ssh-agent as a user service, and user-scoped
|
||||||
|
services in general. The host vocabulary still has no user-scope unit at all; a workstation's
|
||||||
|
per-user daemons (a bar watchdog, a config reloader, an audio service masked per user) have no form
|
||||||
|
the mesh can send.
|
||||||
|
|
||||||
|
**A gap this surfaced:** §1 shipped as code before it had a decision record. The account as a node
|
||||||
|
fact, the home as a placement root, and what the mesh may and may not do under a home are each a
|
||||||
|
decision this document names but no record states. They are the next records to write, before the
|
||||||
|
family of §2 modules is built.
|
||||||
|
|
||||||
## Why now, and why not yet
|
## Why now, and why not yet
|
||||||
|
|
||||||
**Why it matters:** when HAL retires, the generators that keep `~/.ssh`, shell config and the
|
**Why it matters:** when HAL retires, the generators that keep `~/.ssh`, shell config and the
|
||||||
@@ -137,7 +179,7 @@ alias and its trust, and a fresh machine has no operator dotfiles at all — the
|
|||||||
service and leave the human unable to work on the box.
|
service and leave the human unable to work on the box.
|
||||||
|
|
||||||
**Why not build it reflexively:** it is a real addition to the node model, the resource model, and
|
**Why not build it reflexively:** it is a real addition to the node model, the resource model, and
|
||||||
the seat set, and must be gotten right. The mechanism half is now settled — ADR 0128 is what lets
|
the seat set, and must be gotten right. The mechanism half is now settled — ADR 0120 is what lets
|
||||||
the ssh files be templates with no control-plane format — so what remains to decide here is the
|
the ssh files be templates with no control-plane format — so what remains to decide here is the
|
||||||
model:
|
model:
|
||||||
|
|
||||||
@@ -156,15 +198,22 @@ model:
|
|||||||
unnecessary, and forwarding an agent into a node exposes the operator's keys to that node's root —
|
unnecessary, and forwarding an agent into a node exposes the operator's keys to that node's root —
|
||||||
so prefer certificates and `ProxyJump` over forwarding.
|
so prefer certificates and `ProxyJump` over forwarding.
|
||||||
|
|
||||||
**Not urgent, not blocking.** ssh and dotfiles work today because HAL's generators still run as the
|
**Now load-bearing.** The migration of every node to the mesh is complete; what remains of the
|
||||||
substrate. This becomes load-bearing in the node-by-node retirement phase, not before — which is the
|
predecessor is exactly the user environment this design covers — ssh config, dotfiles, the desktop
|
||||||
right time to build it, once the account and CA model are decided here.
|
stack and the per-user services of the two workstations. Those generators are the last thing
|
||||||
|
keeping the predecessor running, so the model questions above are no longer deferred: the account
|
||||||
|
record, the home as a placement root, user-scoped services and the one-off steps a hook used to run
|
||||||
|
each need a decision before the modules that replace the generators can be written.
|
||||||
|
|
||||||
|
## The family beyond `~/.ssh` — 2026-10-02
|
||||||
|
|
||||||
|
The modules §2 calls *a family* — the shell, the terminal, the desktop, everything under a home that is not `~/.ssh` — are designed in [37 — The operator's machine](37-the-operators-machine.md), under [ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md) to [0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md). This document keeps `~/.ssh`, the CA and the roster files. Two things it listed as not built are decided there: user-scoped services (ADR 0177) and the one-off steps a hook used to run (declared state, or a seat's verb).
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- The gap was found generating `~/.ssh/config` from the *HAL* registry (`hal/terminal`'s
|
- The gap was found generating `~/.ssh/config` from the *HAL* registry (`hal/terminal`'s
|
||||||
postConfigure hook), which the nox mesh has no equivalent for.
|
postConfigure hook), which the nox mesh has no equivalent for.
|
||||||
- [ADR 0128](../../02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md) — the roster
|
- [ADR 0120](../../02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md) — the roster
|
||||||
fact mechanism that renders the ssh files, format owned by the module.
|
fact mechanism that renders the ssh files, format owned by the module.
|
||||||
- [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) — the
|
- [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) — the
|
||||||
system-path placement this mirrors for home paths.
|
system-path placement this mirrors for home paths.
|
||||||
@@ -173,6 +222,6 @@ right time to build it, once the account and CA model are decided here.
|
|||||||
- [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md) — the CA key is a secret the
|
- [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md) — the CA key is a secret the
|
||||||
vault makes; [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md)
|
vault makes; [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md)
|
||||||
— short-lived certs as rotation.
|
— short-lived certs as rotation.
|
||||||
- [ADR 0125](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md),
|
- [ADR 0117](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md),
|
||||||
[ADR 0126](../../02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md) —
|
[ADR 0118](../../02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md) —
|
||||||
the never-sever-the-channel rule and the found-vs-owned semantics, applied here to `~/.ssh`.
|
the never-sever-the-channel rule and the found-vs-owned semantics, applied here to `~/.ssh`.
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: proposed
|
status: proposed
|
||||||
code: []
|
code: []
|
||||||
updated: 2026-09-27
|
updated: 2026-10-01
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md
|
||||||
|
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||||
- 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
|
- 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
|
||||||
- 02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md
|
- 02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md
|
||||||
---
|
---
|
||||||
@@ -114,6 +116,19 @@ automate the freeze.
|
|||||||
(this is how the uplink managers and the re-registrations above were done). Only image-bearing
|
(this is how the uplink managers and the re-registrations above were done). Only image-bearing
|
||||||
modules need the build machine, which narrows what the deadlock above can block.
|
modules need the build machine, which narrows what the deadlock above can block.
|
||||||
|
|
||||||
|
## What a merge does now (2026-10-01)
|
||||||
|
|
||||||
|
Revision, [ADR 0162](../../02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md). The
|
||||||
|
trigger exists: the forge announces a merge on the bus and the controller acts on it (ADR 0157 made
|
||||||
|
the build narrate; this makes the merge a plan). A module's dependencies are one relation in the
|
||||||
|
catalogue — `depends-on` edges of four kinds: stands-on, packages, built-by, declared. A merge takes
|
||||||
|
what changed and everything reachable from it along those edges, sorts the set into tiers, writes the
|
||||||
|
plan to the store, asks the first tier and returns. Each outcome advances the plan; a tier whose
|
||||||
|
rolled-out modules a later tier is built by waits until the machines report them applied; a
|
||||||
|
controller replaced mid-plan resumes from the store. `status` lists open plans and names one that
|
||||||
|
has waited too long. The transition discipline for breaking changes in the list above is still
|
||||||
|
unwritten, and still the next thing.
|
||||||
|
|
||||||
## Why now, and why not yet
|
## Why now, and why not yet
|
||||||
|
|
||||||
**Why it matters:** self-update is the difference between a mesh a person maintains by typing
|
**Why it matters:** self-update is the difference between a mesh a person maintains by typing
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ code:
|
|||||||
- mesh-catalog modules/mesh-catalog
|
- mesh-catalog modules/mesh-catalog
|
||||||
updated: 2026-09-28
|
updated: 2026-09-28
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||||
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
||||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||||
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
|
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
|
||||||
@@ -120,6 +121,12 @@ and a module consuming one event from two emitters could tell them apart only by
|
|||||||
The subject already carries the emitter, so the key a module sees names it too — which makes a
|
The subject already carries the emitter, so the key a module sees names it too — which makes a
|
||||||
disagreement between a manifest and the code a typo rather than a category error.
|
disagreement between a manifest and the code a typo rather than a category error.
|
||||||
|
|
||||||
|
*2026-10-01 ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)):* where a name lands is now
|
||||||
|
*issued* to each assignment as a membership the controller publishes, rather than derived by a rule
|
||||||
|
the runtime carries; a module still declares only names, and gains one fact about itself — whether its
|
||||||
|
instances are interchangeable — which decides whether the mesh issues it the module's plain subject
|
||||||
|
beside its machine's.
|
||||||
|
|
||||||
## 2. Three namespaces, and nothing else
|
## 2. Three namespaces, and nothing else
|
||||||
|
|
||||||
**Its own** — `mesh.mod.<module>.>`. Its events and its tools. Nothing else may publish into it,
|
**Its own** — `mesh.mod.<module>.>`. Its events and its tools. Nothing else may publish into it,
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: in-progress
|
status: implemented
|
||||||
code: [mesh-controller, mesh-tools]
|
code: [mesh-controller, mesh-tools]
|
||||||
updated: 2026-09-30
|
updated: 2026-10-02
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md
|
||||||
|
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||||
|
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
|
||||||
- 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
|
- 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
|
||||||
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||||
- 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
|
- 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
|
||||||
@@ -79,6 +82,16 @@ machine's holder and the holders' queue group would hand the call to whichever a
|
|||||||
node-scoped seat's tool therefore carries the node it is asked of. Nothing about a mesh-scoped seat
|
node-scoped seat's tool therefore carries the node it is asked of. Nothing about a mesh-scoped seat
|
||||||
changes.
|
changes.
|
||||||
|
|
||||||
|
*2026-10-01 ([ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)):*
|
||||||
|
the same shape now serves a **module's** tool on several machines, which had the queue-group fault
|
||||||
|
this section describes for seats: each instance also serves `mesh.mod.<module>.tool.<tool>.<node>`,
|
||||||
|
a caller writes `<module>.<tool>@<node>`, and every answer names the machine that gave it. And §3 is
|
||||||
|
built for every holder, not only the controller: the credential names the seats a module claims and
|
||||||
|
their verbs, the runtime serves each with the tool of the same name on the seat's subject, and the
|
||||||
|
bus admits it only where the module holds the seat. *Later the same day ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)):*
|
||||||
|
the subjects a holder serves, and a module's own, stop being derived in the runtime and are issued to
|
||||||
|
the assignment as a membership the controller publishes; the shape stays, the deciding moves.
|
||||||
|
|
||||||
## 5. Discovery
|
## 5. Discovery
|
||||||
|
|
||||||
**What a role answers is a read.** The seats and their protocols are records, so the list is a query
|
**What a role answers is a read.** The seats and their protocols are records, so the list is a query
|
||||||
@@ -109,6 +122,8 @@ module-specific names that changes the day the forge is replaced.
|
|||||||
asked of the module, through a `tools` verb every runtime answers) and lists a role's tools when the
|
asked of the module, through a `tools` verb every runtime answers) and lists a role's tools when the
|
||||||
records carry them.
|
records carry them.
|
||||||
|
|
||||||
|
*Amended 2026-10-02 by [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md):* the module that serves this to an agent is the node tools runtime — one per node, host-side, serving every assigned module's tools as well as answering the person on loopback. The console is its serving mode, renamed. See [37 — The operator's machine](37-the-operators-machine.md) §3.
|
||||||
|
|
||||||
## 7. Versioning
|
## 7. Versioning
|
||||||
|
|
||||||
A seat's tools are an interface and change like one. Additive within a version. A change that would
|
A seat's tools are an interface and change like one. Additive within a version. A change that would
|
||||||
@@ -139,6 +154,36 @@ verb answers every seat's tools from the records, because the console cannot rea
|
|||||||
console lists a role's tools beside the modules' own and resolves `<seat>.<verb>` to the seat when the
|
console lists a role's tools beside the modules' own and resolves `<seat>.<verb>` to the seat when the
|
||||||
seat declares that verb. Which verbs each *other* seat serves stays a decision per seat, still untaken.
|
seat declares that verb. Which verbs each *other* seat serves stays a decision per seat, still untaken.
|
||||||
|
|
||||||
|
## What shipped, 2026-09-30
|
||||||
|
|
||||||
|
mesh-controller PRs 166 and 167, mesh-tools PR 21. Verified on the live mesh the same evening: the
|
||||||
|
console on a workstation lists the twelve verbs as `mesh-controller.<verb>` beside 67 module tools, and
|
||||||
|
`mesh-controller.nodes` and `mesh-controller.status` answer through it with what the commands print.
|
||||||
|
|
||||||
|
Two things shipped bent. **The grant arrived after the holder started**: the controller composes the
|
||||||
|
bus's user list and a push delivers it, so the first controller to serve its seat subscribed before
|
||||||
|
the broker's list named the grant, the server refused all twelve subscriptions, and the client never
|
||||||
|
retried — a holder now rebinds a refused subscription every thirty seconds (PR 167), and a controller
|
||||||
|
roll-out that adds a grant is followed by a push to the broker node. **A JSON verb's answer was parsed
|
||||||
|
from both output streams**, so `status --json`'s warnings hid the document as data; the answer is now
|
||||||
|
parsed from standard output alone (mesh-controller PR 168, pending). The `output` field carried it
|
||||||
|
either way.
|
||||||
|
|
||||||
|
What stays as designed and not built: which verbs any *other* seat serves, and §3 for module-declared
|
||||||
|
seats' schemas beyond the names their manifests already list.
|
||||||
|
|
||||||
|
## The firewall seat's verbs, 2026-10-02
|
||||||
|
|
||||||
|
[ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md). The first node-scoped seat
|
||||||
|
to carry verbs: `node-packet-filter` serves `rules` (the filter as the machine enforces it, nftables
|
||||||
|
and legacy), `reload` (the mesh's own filter from its file) and `remove` (one rule set the mesh did
|
||||||
|
not write, named as the host reports it under ADR 0168; refusing the mesh's tables, the runtime's
|
||||||
|
own chains, a built-in chain and an active found firewall's). Every holder serves all three; the
|
||||||
|
nftables module does so from a runtime on the machine's network with `NET_ADMIN`, which is the first
|
||||||
|
container to declare a capability. Removing a predecessor's rule set is an operator's act reached
|
||||||
|
through the seat, recorded on the bus, instead of a shell on the machine. *How it is checked:* ADR
|
||||||
|
0169's table.
|
||||||
|
|
||||||
## What this does not settle
|
## What this does not settle
|
||||||
|
|
||||||
- Which verbs each seat should serve. That is a decision per seat, and the reason to do it slowly: a
|
- Which verbs each seat should serve. That is a decision per seat, and the reason to do it slowly: a
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: implemented
|
status: implemented
|
||||||
code: [mesh-catalog, mesh-tools, mesh-controller]
|
code: [mesh-catalog, mesh-tools, mesh-controller]
|
||||||
updated: 2026-09-30
|
updated: 2026-10-02
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||||
|
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
|
||||||
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
||||||
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||||
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
|
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
|
||||||
@@ -18,6 +20,8 @@ An agent reaches them over MCP on the machine's loopback; a person reaches the s
|
|||||||
installed by hand, nothing is configured with an address, and the mesh knows the surface exists because
|
installed by hand, nothing is configured with an address, and the mesh knows the surface exists because
|
||||||
it put it there ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
|
it put it there ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
|
||||||
|
|
||||||
|
> **Amended 2026-10-02 by [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** What this document describes stays true in substance and changes in form: the console becomes the serving mode of the node tools runtime, a host-side process the host supervises rather than a container, which also serves every assigned module's tools from their bundles. The module is renamed `node-tools`. [37 — The operator's machine](37-the-operators-machine.md) §3 is where it now lives.
|
||||||
|
|
||||||
## 1. What it is
|
## 1. What it is
|
||||||
|
|
||||||
A module, `mesh-console`, in the catalogue. Its image is the tool runtime's own — the client that
|
A module, `mesh-console`, in the catalogue. Its image is the tool runtime's own — the client that
|
||||||
@@ -64,6 +68,14 @@ once a request nothing serves, so a module that is not running costs nothing and
|
|||||||
as not answering, rather than silently absent — *silence and success must never look alike*. The list is
|
as not answering, rather than silently absent — *silence and success must never look alike*. The list is
|
||||||
kept for a short while and refreshed, so an agent asking on every turn does not fan out on every turn.
|
kept for a short while and refreshed, so an agent asking on every turn does not fan out on every turn.
|
||||||
|
|
||||||
|
**Every module tool takes the machine to ask** (*2026-10-01*, [ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)):
|
||||||
|
an optional `node` the console lists on each one, puts into the subject and never hands to the module,
|
||||||
|
for a module that runs on several machines; without it whichever instance answers first does, and the
|
||||||
|
console appends *answered by <machine>* to every answer. A seat's verb takes none; the seat's scope
|
||||||
|
decides. *Later the same day ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)):* the console composes no
|
||||||
|
subject at all; each tool's subject comes with the listing, and a stateful module on two machines is
|
||||||
|
listed once per machine because the mesh issued it no plain subject.
|
||||||
|
|
||||||
**A tool that was not listed can still be called.** Listing is discovery; calling is the grant. An agent
|
**A tool that was not listed can still be called.** Listing is discovery; calling is the grant. An agent
|
||||||
that knows a tool's name asks for it by `<module>.<tool>` and the module answers or the bus says why not.
|
that knows a tool's name asks for it by `<module>.<tool>` and the module answers or the bus says why not.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: in-progress
|
status: implemented
|
||||||
code: [mesh-catalog modules/records]
|
code: [mesh-catalog modules/records]
|
||||||
updated: 2026-09-30
|
updated: 2026-09-30
|
||||||
decisions:
|
decisions:
|
||||||
@@ -71,6 +71,20 @@ harmless. The mesh session of design 15, when it exists, calls this rather than
|
|||||||
| a sync against an unreachable origin leaves the checkout standing and says why | silence and success never look alike |
|
| a sync against an unreachable origin leaves the checkout standing and says why | silence and success never look alike |
|
||||||
| live: through the console, `records_search` for a phrase that appears only in a design document here returns it | issue 006's closing check |
|
| live: through the console, `records_search` for a phrase that appears only in a design document here returns it | issue 006's closing check |
|
||||||
|
|
||||||
|
## What shipped, 2026-09-30
|
||||||
|
|
||||||
|
mesh-catalog PR 183, then PR 185. Verified on the live mesh the same evening: `records` assigned to the
|
||||||
|
control node with `{"repository": …}` as its setting, its checkout at the repository's `main` with 478
|
||||||
|
documents, its five tools listed by the console beside every other tool, and — ADR 0025's check —
|
||||||
|
`records_search` for a phrase from this document's title returned it from where it is written, with
|
||||||
|
the commit. The first live search missed: the phrase chosen from ADR 0025 straddled a line break under
|
||||||
|
emphasis, and the reader matched single lines. PR 185 matches a line together with the next and
|
||||||
|
ignores emphasis marks, which the module's test now covers; until it rolls, a phrase that wraps is one
|
||||||
|
to shorten.
|
||||||
|
|
||||||
|
The reader's `consumes` names the forge module's event rather than the `git` seat, because the seat
|
||||||
|
declares none; a merge into the repository was seen and pulled within seconds.
|
||||||
|
|
||||||
## What this does not settle
|
## What this does not settle
|
||||||
|
|
||||||
- Ranking or meaning. A search that understands a question is the session's job, not the reader's.
|
- Ranking or meaning. A search that understands a question is the session's job, not the reader's.
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
---
|
||||||
|
layer: to-be
|
||||||
|
status: in-progress
|
||||||
|
code: [mesh-host, mesh-controller, mesh-tools, mesh-catalog]
|
||||||
|
updated: 2026-10-02
|
||||||
|
decisions:
|
||||||
|
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
|
||||||
|
- 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md
|
||||||
|
- 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
|
||||||
|
- 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md
|
||||||
|
- 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
|
||||||
|
- 02-DECISIONS/0040-what-a-module-is.md
|
||||||
|
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||||
|
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
||||||
|
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||||
|
- 02-DECISIONS/0161-what-deserves-a-seat.md
|
||||||
|
- 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 37 — The operator's machine
|
||||||
|
|
||||||
|
**Every configurable thing on a node is a module, the home included, and the same catalogue serves
|
||||||
|
a server and a laptop.** One default configuration per module, varied per node by a setting or a
|
||||||
|
kept region; roles a machine has once as node-scoped seats with tool contracts; one tool runtime
|
||||||
|
per node serving every module's tools on the host side
|
||||||
|
([ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md)
|
||||||
|
to [0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)).
|
||||||
|
This is the design [to-be 29](29-a-node-has-operator-accounts.md) §2 called *a family* and
|
||||||
|
[research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md) measured.
|
||||||
|
|
||||||
|
## 1. What a module of the environment looks like
|
||||||
|
|
||||||
|
Worked on the first one, a shell. The `zsh` module declares:
|
||||||
|
|
||||||
|
- a **package**, `zsh`;
|
||||||
|
- **files under the home**, owned by the account: the shell's rc file with the module's default
|
||||||
|
configuration, carrying a kept region for the operator's own lines, and `${setting:…}`
|
||||||
|
placeholders for the few values a node varies; the account and its home are machine facts the
|
||||||
|
controller resolves ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md),
|
||||||
|
to-be 29 §2);
|
||||||
|
- a **seat declaration**, `login-shell`, node-scoped, with its one verb; and a **claim** on it;
|
||||||
|
- a **`user` shape** naming the shell, applied only where the module holds the seat;
|
||||||
|
- a **tools bundle**, the artifact kind for interpreted code, with `execute` and the module's own
|
||||||
|
`show-config`.
|
||||||
|
|
||||||
|
No container, no unit, no service. It is assigned to every node with an operator account. The
|
||||||
|
`fish` and `bash` modules are the same with another package and other files; one of the three
|
||||||
|
holds the seat on each node.
|
||||||
|
|
||||||
|
The second shape is **system scope**: the login manager declares a package, two files under
|
||||||
|
`/etc`, and a service, which is exactly what the ssh daemon module declares today. The third
|
||||||
|
shape is **graphical**: the window manager declares a package, files under the home, a
|
||||||
|
user-scoped unit or two, a claim on the display-session seat, a dependency on the display server
|
||||||
|
being held, and a bundle with its tools. Nothing in any of them says which machine it is for.
|
||||||
|
|
||||||
|
## 2. Variation
|
||||||
|
|
||||||
|
A node differs from the default in two ways and no other
|
||||||
|
([ADR 0174](../../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)):
|
||||||
|
a **setting** the module declared, set in the node's layer and rendered into the file; or lines in
|
||||||
|
a **kept region** the file marks. The predecessor's ninety theme variables become the settings of
|
||||||
|
the modules whose files read them. Until the settings record proposed alongside the
|
||||||
|
container-runtime records ships — a setting names the file it lands in — environment modules carry
|
||||||
|
defaults in their files and declare no setting; that is the order, not a preference.
|
||||||
|
|
||||||
|
## 3. The node tools runtime
|
||||||
|
|
||||||
|
One per node, started and restarted by the host as a sibling process, never a container
|
||||||
|
([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
|
||||||
|
It is the tool runtime that exists, in the role it was written for: it reads the memberships of
|
||||||
|
every module assigned to the node, loads each module's tools bundle, and serves every tool and
|
||||||
|
every held seat's verb on the subjects issued. It holds the node's one bus credential and may call
|
||||||
|
every tool on the mesh. Its serving mode on the machine's loopback is what the console was
|
||||||
|
([to-be 34](34-the-console.md)); the module is renamed **node-tools** and declares the interpreter
|
||||||
|
it needs as a package.
|
||||||
|
|
||||||
|
A bundle reaches the node as any artifact does. A push that adds or replaces one is a reload. A
|
||||||
|
bundle that fails to load is named in the node's report and the others serve. A tool that needs
|
||||||
|
root escalates itself.
|
||||||
|
|
||||||
|
## 4. The seats of the environment
|
||||||
|
|
||||||
|
Decided now: **`login-shell`** (module-declared; zsh, fish, bash; verb `execute`) and
|
||||||
|
**`node-service-manager`** (the mesh's own; systemd; verbs over units in both scopes). The rest
|
||||||
|
are candidates from [research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md),
|
||||||
|
one record each when its first holder is written: display server, display session, terminal
|
||||||
|
emulator, launcher, notifier, compositor, lock screen, bar, login manager, audio, clipboard, boot.
|
||||||
|
Editors, browsers, media players, the agent, the downloads and scripts folders are modules with
|
||||||
|
tools and no seat.
|
||||||
|
|
||||||
|
A module that needs a role filled depends on **the seat being held** on the node, not on a
|
||||||
|
capability: the window manager needs the display server seat held, by xorg or by a compositor
|
||||||
|
that is its own server. Whether a held seat can gate an assignment is the first question the
|
||||||
|
resolver is asked by the second graphical module; the display server itself is gated by the
|
||||||
|
`graphical-session` capability the profile already reports.
|
||||||
|
|
||||||
|
## 5. What the host gains, and what it does not
|
||||||
|
|
||||||
|
- `service` gains `scope: user`, applied as the account
|
||||||
|
([ADR 0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)).
|
||||||
|
- The host starts and supervises the node tools runtime as it would any host-side process, and
|
||||||
|
delivers bundles as artifacts.
|
||||||
|
- Nothing else. No hooks, no actions: `chsh` is the `user` shape, enabling a unit is the `service`
|
||||||
|
shape, rebuilding boot images is a verb of the boot seat when that seat is written.
|
||||||
|
- A gap, recorded: the `package` shape drives the distribution's package manager and nothing
|
||||||
|
outside its repositories. The login manager in use is such a package; it waits on an official
|
||||||
|
package or a decision the host does not yet have.
|
||||||
|
|
||||||
|
## 6. The order of the build
|
||||||
|
|
||||||
|
1. **The operator account on every node** — `mesh-controller node` with the login name; empty on
|
||||||
|
all four today. Nothing home-scoped composes before it.
|
||||||
|
2. **The node tools runtime** — mesh-host supervises it; mesh-tools serves bundles from memberships
|
||||||
|
and reloads; mesh-controller composes the bundle into the declaration and the memberships to one
|
||||||
|
runtime per node; the catalogue renames the console. Proven when the packet-filter verbs answer
|
||||||
|
from it and its container is gone.
|
||||||
|
3. **`zsh`**, the first environment module: seat, `user` shape, home files, `execute`. Proven on a
|
||||||
|
server first, then every node.
|
||||||
|
4. **`systemd`** and user scope: the host's field, the seat seeded, the module. Proven by the
|
||||||
|
desktop's reload watcher declared `scope: user` on a workstation.
|
||||||
|
5. **The login manager**, system scope, once its package is installable; then the display server,
|
||||||
|
the window manager, and the rest of the graphical stack, each seat its own record.
|
||||||
|
6. **Settings** for the theme knobs, after the settings record ships and issue 168 closes.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Claim | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A module with a package, home files, a seat and a bundle resolves and composes on a node with an account, and is refused on one without | the controller's composition tests |
|
||||||
|
| One runtime per node serves every assigned module's tools; a per-module tool container no longer exists | the runtime's tests; `docker ps` on a converged machine |
|
||||||
|
| A user-scoped unit is applied as the account | the host's tests |
|
||||||
|
| A node's difference from a module's default is visible as a setting with a source or a kept region | `mesh-controller.settings`; the host's write-into tests |
|
||||||
|
| The same manifests assign to a server and a workstation; the graphical ones are refused on the server by name | the resolver's tests and the live mesh |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md)
|
||||||
|
- [To-be 29](29-a-node-has-operator-accounts.md) — the account and the home; this design is the
|
||||||
|
family its §2 names, beyond `~/.ssh`.
|
||||||
|
- [To-be 33](33-the-tools-the-mesh-answers.md), [to-be 34](34-the-console.md) — the tools and
|
||||||
|
the console, amended by ADR 0175.
|
||||||
|
- [To-be 05](05-the-node-host.md) — the host's vocabulary, widened by ADR 0177.
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
---
|
||||||
|
layer: to-be
|
||||||
|
status: in-progress
|
||||||
|
code: [mesh-tools, mesh-controller, mesh-host, mesh-catalog]
|
||||||
|
updated: 2026-10-02
|
||||||
|
decisions:
|
||||||
|
- 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
|
||||||
|
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
|
||||||
|
- 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md
|
||||||
|
- 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md
|
||||||
|
- 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
|
||||||
|
---
|
||||||
|
|
||||||
|
# 38. Building the operator's machine
|
||||||
|
|
||||||
|
**The work of [design 37](37-the-operators-machine.md), broken into packages small enough that each
|
||||||
|
ends at something a person can see run, in the order their dependencies allow.** Design 37 is the
|
||||||
|
authority on *what* is built; this document holds only the packages, their order, their sizes and
|
||||||
|
their proofs, and is wrong the moment it disagrees with 37 rather than the other way round. It is
|
||||||
|
the shape [design 28](28-building-the-bus.md) gave the bus work, applied here.
|
||||||
|
|
||||||
|
## How this is built, and where it is run
|
||||||
|
|
||||||
|
**On the live mesh, by the operator's decision.** Every package is written with unit tests and
|
||||||
|
committed on one branch per repository; its proof runs on the four machines, not in the lab.
|
||||||
|
[ADR 0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md) already says the live mesh is
|
||||||
|
the test bed; the operator's words on 2026-10-02 were *skip the lab, it is not too bad if something
|
||||||
|
is broken*. The cost accepted: a package that breaks the runtime breaks every tool on a node until
|
||||||
|
the next push, and the controller's own verbs stay reachable through the controller seat whatever
|
||||||
|
happens to a node's runtime — which is the one thing that must hold, and does by construction
|
||||||
|
([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)).
|
||||||
|
|
||||||
|
Each package names what proves it. A package that cannot name its proof is divided until it can.
|
||||||
|
|
||||||
|
## What exists already, measured
|
||||||
|
|
||||||
|
Counted 2026-10-02 in the four repositories, non-test source. The point of the count is the same
|
||||||
|
as design 28's: nothing here is new ground; every package reshapes something standing.
|
||||||
|
|
||||||
|
| Piece | Today | Size | Becomes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| the tool runtime | TypeScript: loads `MESH_TOOL_MODULES`, serves one module's tools and its claimed seats' verbs; `serve` is the console | ~1 700 lines over six files | loads every assigned module's bundle; `serve` is node tools |
|
||||||
|
| the host's `process` shape | Go: fetch a bundle by digest, unpack under the mesh's daemons directory, write the unit, run it | 343 lines | **unchanged** — the runtime is one such process |
|
||||||
|
| the host's `archive` shape | Go: fetch and unpack an artifact at a path | 185 lines | **unchanged** — a module's tools bundle is one such archive |
|
||||||
|
| the controller's bus principals | Go: one principal per module per node, grants from what it declares | 132 lines | gains one principal per node for the runtime |
|
||||||
|
| the controller's memberships | Go: one per assignment, the subjects a runtime serves | 143 lines | **unchanged** in shape; the runtime reads several |
|
||||||
|
| the controller's declaration composer | Go, one file | 2 053 lines | gains the runtime's process, the bundles' archives, two env words |
|
||||||
|
| the catalogue | 35 manifests build a per-module tool container on the runtime's base image | — | none do; the runtime is a module of its own |
|
||||||
|
|
||||||
|
**Two measurements decide the shape.** The host needs no change: a `process` and an `archive` are
|
||||||
|
what the runtime and a bundle are, and both are applied today. And the runtime already does
|
||||||
|
nine-tenths of the job — the loop over entrypoints, the seat verbs, the membership subscription —
|
||||||
|
for one module; the work is to let it do the same for a list.
|
||||||
|
|
||||||
|
## The order the work allows
|
||||||
|
|
||||||
|
```
|
||||||
|
WP1 the runtime serves many modules (mesh-tools) ──┐
|
||||||
|
WP2 the controller composes one runtime a node (mesh-controller) ──┤ independent, test-proven
|
||||||
|
│
|
||||||
|
WP3 the runtime is a module; the console is its serving mode (mesh-tools, mesh-catalog)
|
||||||
|
│
|
||||||
|
WP4 the first holder moves: the packet filter (mesh-catalog) ── the live proof
|
||||||
|
│
|
||||||
|
WP5 the shell, on a server (mesh-catalog) ── the first environment module live
|
||||||
|
WP6 the service manager, on a workstation (mesh-host #72, mesh-catalog)
|
||||||
|
│
|
||||||
|
WP7 the login manager, the display server, the window manager … ── one record per seat, after this document
|
||||||
|
WP8 settings for the theme knobs ── after issue 168 closes
|
||||||
|
```
|
||||||
|
|
||||||
|
WP1 and WP2 touch different repositories and meet only at the membership's shape, which neither
|
||||||
|
changes; they are built in parallel. WP3 needs both. WP4 is the first time anything on a machine
|
||||||
|
changes, and it is the proof of the whole. WP5 and WP6 are the first environment modules; the
|
||||||
|
packages after them are design 37 §4's candidates and are not broken down here, because each
|
||||||
|
begins with a decision record this document cannot anticipate.
|
||||||
|
|
||||||
|
## WP1 — The runtime serves many modules
|
||||||
|
|
||||||
|
*mesh-tools. About a day.*
|
||||||
|
|
||||||
|
**What changes.** `serve` takes a list of modules to serve, each with its entrypoints, rather than
|
||||||
|
one module and one credential. The runtime reads one membership per module from the subjects
|
||||||
|
[ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||||
|
derives for each, and serves each module's tools on that module's subjects and each held seat's
|
||||||
|
verbs on the seat's. The filter that drops a registration under any name but the one module goes;
|
||||||
|
what remains is the rule that a registration under a seat's name is served only where some module
|
||||||
|
the runtime serves claims that seat. A bundle that throws on import is named in the log and in
|
||||||
|
what `tools` answers, and the others serve. The runtime reads `MESH_OPERATOR_ACCOUNT` and
|
||||||
|
`MESH_OPERATOR_HOME` and hands them to every tool's environment.
|
||||||
|
|
||||||
|
**What does not change.** The SDK. The broker client. The MCP surface. A module's tool code.
|
||||||
|
|
||||||
|
**Proof.** The runtime's test against a real bus: three bundles, one of which throws on import;
|
||||||
|
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.
|
||||||
|
|
||||||
|
## WP2 — The controller composes one runtime per node
|
||||||
|
|
||||||
|
*mesh-controller. Two to three days; the largest package.*
|
||||||
|
|
||||||
|
**What changes**, in four pieces, each its own commit:
|
||||||
|
|
||||||
|
1. **A node principal.** Beside one principal per module per node, one per node of kind
|
||||||
|
`node-tools`: its serving grants are the union of every assigned module's tool subjects and every
|
||||||
|
held seat's verbs on that node, its invoking grant is `*`, and it consumes nothing. The
|
||||||
|
per-module memberships are composed as today; nothing else on the bus learns a new shape.
|
||||||
|
2. **Bundle delivery.** For every assigned module whose build produced a `bundle`, the node's
|
||||||
|
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, `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.
|
||||||
|
|
||||||
|
**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
|
||||||
|
memberships as before. The gate's test: the packet-filter manifest as it is today is refused once
|
||||||
|
the runtime is registered.
|
||||||
|
|
||||||
|
## WP3 — The runtime is a module, and the console is its serving mode
|
||||||
|
|
||||||
|
*mesh-tools and mesh-catalog. A day.*
|
||||||
|
|
||||||
|
**What changes.** mesh-tools gains a `bundle` artifact of itself beside its images, and its manifest
|
||||||
|
becomes the `node-tools` module: a package for the interpreter, the loopback listener the console
|
||||||
|
declared, `invokes: *`, and nothing else — the process is the controller's to compose (WP2). In the
|
||||||
|
catalogue, `mesh-console` is retired as a module and `node-tools` assigned where it was. The
|
||||||
|
runtime's `serve` keeps answering MCP on loopback; the person's end of it keeps the name *console*
|
||||||
|
([glossary](../../00-META/glossary.md)).
|
||||||
|
|
||||||
|
**Proof.** On every node: the console's container is gone, `node-tools` runs as a unit the host
|
||||||
|
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`.
|
||||||
|
|
||||||
|
## WP4 — The first holder moves: the packet filter
|
||||||
|
|
||||||
|
*mesh-catalog. Half a day. The live proof of ADR 0175.*
|
||||||
|
|
||||||
|
**What changes.** The nftables module drops its container, its `NET_ADMIN` and its runtime
|
||||||
|
artifact; its tools bundle stays and its claim stays. Its `remove` and `reload` escalate inside the
|
||||||
|
tool where they need root, which they have, since the runtime runs as the node's account.
|
||||||
|
|
||||||
|
**Proof.** `node-packet-filter.rules@<node>`, `reload` and `remove` answer from the runtime on all
|
||||||
|
four machines; `docker ps` shows no `mesh-nftables`; `status` is well. Then the fail2ban holder
|
||||||
|
proposed in an open change follows the same way when it lands.
|
||||||
|
|
||||||
|
## WP5 — The shell, on a server first
|
||||||
|
|
||||||
|
*mesh-catalog #224, already written. Half a day to assign and prove.*
|
||||||
|
|
||||||
|
**Order.** Assign `zsh` to one server; push; `login-shell.execute@<server> command="uptime"`
|
||||||
|
answers; the account's login shell reads zsh; its `~/.zshrc` carries the mesh's block with the
|
||||||
|
operator's lines around it. Then the other three nodes. The two things the manifest cannot say
|
||||||
|
— the `user` shape applying only where the seat is held, and a second shell module installed
|
||||||
|
beside the holder — are the first follow-up record after this document.
|
||||||
|
|
||||||
|
## WP6 — The service manager, on a workstation
|
||||||
|
|
||||||
|
*mesh-host #72 merged first; mesh-catalog #224. Half a day.*
|
||||||
|
|
||||||
|
**Order.** Merge the host's user-scope change and let it roll. Assign `systemd` everywhere;
|
||||||
|
`node-service-manager.units@<node> scope=user` answers on a workstation. Then the first user-scoped
|
||||||
|
unit the mesh sends: the window manager's reload watcher, declared `scope: user` by the window
|
||||||
|
manager module when WP7 writes it — until then, the host's change is proven by its tests and by
|
||||||
|
the verb answering.
|
||||||
|
|
||||||
|
## What is deliberately not here
|
||||||
|
|
||||||
|
- **The graphical stack's seats** (WP7). Each begins with a record naming its holders and verbs,
|
||||||
|
and the first graphical module asks the resolver a question this document cannot answer for it:
|
||||||
|
whether a held seat gates another's assignment.
|
||||||
|
- **Settings for the theme knobs** (WP8). Blocked on the settings record proposed in an open change
|
||||||
|
and on [issue 168](../../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md).
|
||||||
|
- **Reload without restart.** WP2 restarts the runtime on a bundle change; a reload that keeps the
|
||||||
|
other modules' tools up during one module's change is a refinement for after WP4 proves the
|
||||||
|
simple form.
|
||||||
|
- **Lingering.** A user-scoped unit answers only while the account's manager runs; declaring
|
||||||
|
lingering for the account is a field on the `user` shape, decided when a server first needs a
|
||||||
|
user unit.
|
||||||
|
|
||||||
|
## How this list is kept true
|
||||||
|
|
||||||
|
Each package's proof is run on the live mesh when the package is finished and its line here gains
|
||||||
|
the date and the commit, the way [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md)
|
||||||
|
carries *built and proven live*. A package whose proof fails is not reworded; the failure is
|
||||||
|
recorded under it and the package stays open. When WP6 is proven, design 37's status moves to
|
||||||
|
`implemented` for what it covers and this document's to the same.
|
||||||
@@ -38,9 +38,11 @@ document is written and this one's status becomes `implemented`.
|
|||||||
| [`26-the-seats.md`](26-the-seats.md) | **Proposed.** What a mesh can have one of, who fills each, and a seat's holder answering for the provision it delivers — including the `git` seat a build's source can live on | [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)), [ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md), [ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md) |
|
| [`26-the-seats.md`](26-the-seats.md) | **Proposed.** What a mesh can have one of, who fills each, and a seat's holder answering for the provision it delivers — including the `git` seat a build's source can live on | [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)), [ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md), [ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md) |
|
||||||
| [`27-a-module-requires-the-mesh-resolves.md`](27-a-module-requires-the-mesh-resolves.md) | **Proposed.** One concept for everything a module needs: a requirement with a contract, answered by one of four kinds of provider, resolved at assignment or refused. Retires settings, placeholders, facts and paths in definitions | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md), [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)) |
|
| [`27-a-module-requires-the-mesh-resolves.md`](27-a-module-requires-the-mesh-resolves.md) | **Proposed.** One concept for everything a module needs: a requirement with a contract, answered by one of four kinds of provider, resolved at assignment or refused. Retires settings, placeholders, facts and paths in definitions | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md), [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)) |
|
||||||
| [`28-building-the-bus.md`](28-building-the-bus.md) | **Proposed.** The five steps of the bus work in the order their dependencies allow, each ending at a bed — with the surface measured, so no step's size is a guess | [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md), [ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md), [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md) |
|
| [`28-building-the-bus.md`](28-building-the-bus.md) | **Proposed.** The five steps of the bus work in the order their dependencies allow, each ending at a bed — with the surface measured, so no step's size is a guess | [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md), [ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md), [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md) |
|
||||||
| [`29-a-node-has-operator-accounts.md`](29-a-node-has-operator-accounts.md) | **Proposed.** The mesh models machines but not the humans on them: a node gains operator accounts, and a resource may live under a home owned by its account — what would own ~/.ssh, dotfiles and ~/.config when HAL retires | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md) |
|
| [`29-a-node-has-operator-accounts.md`](29-a-node-has-operator-accounts.md) | **In progress.** A node has an operator account and a resource may live under its home — built in the controller; the ssh-client module, the SSH CA, the `~/.ssh` boundary and user-scoped services are not. The account fact still wants its decision record | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md) |
|
||||||
|
|
||||||
| [`32-what-a-module-declares.md`](32-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md), [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md), superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) |
|
| [`32-what-a-module-declares.md`](32-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md), [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md), superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) |
|
||||||
|
| [`37-the-operators-machine.md`](37-the-operators-machine.md) | **In progress.** Every configurable thing on a node is a module, the home included; one default per module varied by settings or kept regions; roles a machine has once as seats with tool contracts; one tool runtime per node on the host side | [ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md), [0174](../../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md), [0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), [0176](../../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md), [0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md) |
|
||||||
|
| [`38-building-the-operators-machine.md`](38-building-the-operators-machine.md) | **In progress.** The work of design 37 as packages: the runtime serves many modules, the controller composes one per node, the console becomes its serving mode, the packet filter moves first, then the shell and the service manager — tested on the live mesh by the operator's decision | [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), [0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md), [0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md) |
|
||||||
|
|
||||||
## Not yet written
|
## Not yet written
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-08-23
|
opened: 2026-08-23
|
||||||
located-in: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
|
located-in: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
|
||||||
fixed-by:
|
fixed-by: ADR 0153; mesh-catalog PR 183 (records), PR 185 (a phrase that wraps)
|
||||||
amended-design: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
|
amended-design: 03-DESIGN/01-to-be/35-reading-the-record.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 006 — This repository is not indexed into the knowledge base, and the claim that it is holds up a decision
|
# 006 — This repository is not indexed into the knowledge base, and the claim that it is holds up a decision
|
||||||
@@ -193,3 +193,16 @@ forge and answering `records_search`, `records_read`, `records_list`, `records_s
|
|||||||
[35 — Reading the record](../../03-DESIGN/01-to-be/35-reading-the-record.md). The module's test runs
|
[35 — Reading the record](../../03-DESIGN/01-to-be/35-reading-the-record.md). The module's test runs
|
||||||
0025's check against a repository it makes; this record closes when the same check passes through the
|
0025's check against a repository it makes; this record closes when the same check passes through the
|
||||||
console on the live mesh, and says so below.
|
console on the live mesh, and says so below.
|
||||||
|
|
||||||
|
## Resolved, 2026-09-30
|
||||||
|
|
||||||
|
The check ADR 0025 names passed on the live mesh: through the console on a workstation,
|
||||||
|
`records_search` for a phrase that appears in one design document here returned that document and the
|
||||||
|
commit it was read at, from a checkout the mesh keeps and nobody copied. What this record asked on
|
||||||
|
2026-08-23 — *does the searcher find it without already suspecting it exists?* — is answered by where
|
||||||
|
the tool sits: in the same list as the forge's and the mesh's own, described as the thing to search
|
||||||
|
before forming a hypothesis. Reachable became surfacing when the surface became a list.
|
||||||
|
|
||||||
|
Open beside it, and not this record's: the mesh has no symptom-indexed memory at all since the
|
||||||
|
cut-over ([as-is 07](../../03-DESIGN/00-as-is/07-knowledge.md) says so), and the lessons of these
|
||||||
|
days are in this repository by hand.
|
||||||
|
|||||||
+11
-2
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-09-22
|
opened: 2026-09-22
|
||||||
located-in: [mesh-controller internal/overlay, mesh-host internal/apply]
|
located-in: [mesh-controller internal/overlay, mesh-host internal/apply]
|
||||||
fixed-by:
|
fixed-by: ADR 0102 (mesh-controller internal/overlay: the runtime file written into, reloaded), issue 128 (the hosts file as a region)
|
||||||
amended-design: 03-DESIGN/01-to-be/05-the-node-host.md
|
amended-design: 03-DESIGN/01-to-be/05-the-node-host.md
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -56,3 +56,12 @@ and nothing checks for it today.
|
|||||||
- Should an adopted node that cannot trust the registry be refused a module that needs to pull?
|
- Should an adopted node that cannot trust the registry be refused a module that needs to pull?
|
||||||
Or should the refusal come earlier, when the node joins?
|
Or should the refusal come earlier, when the node joins?
|
||||||
|
|
||||||
|
## Resolved, 2026-10-02
|
||||||
|
|
||||||
|
The runtime's file is written into and the runtime reloaded, never restarted
|
||||||
|
([ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md), the
|
||||||
|
diagnosis above); the hosts file is a marked region the mesh owns alone
|
||||||
|
([issue 128](../128-the-hosts-file-is-written-whole/00-report.md)). Neither whole file remains. Read
|
||||||
|
into [ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), rule 5,
|
||||||
|
which names the one whole machine-wide file the mesh still writes — its own filter at the
|
||||||
|
distribution's path — as a difference a take shows, not a fault.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-22
|
opened: 2026-09-22
|
||||||
located-in: []
|
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
|
||||||
fixed-by:
|
fixed-by: mesh-controller 201 (the preview names the narrowing and the port's reach), 206 (`take --yes <digest>` acts on the preview read)
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -35,3 +35,18 @@ it changes before it changes it, and for taking a module this one does not.
|
|||||||
included, and ask for the same kind of confirmation as the flip?
|
included, and ask for the same kind of confirmation as the flip?
|
||||||
- Or should taking refuse while a port of the module is reachable more widely than the module
|
- Or should taking refuse while a port of the module is reachable more widely than the module
|
||||||
declares, until the operator either changes the module's exposure or confirms the narrowing?
|
declares, until the operator either changes the module's exposure or confirms the narrowing?
|
||||||
|
|
||||||
|
## Decided, 2026-10-01
|
||||||
|
|
||||||
|
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 2: the preview names a narrowing. Building follows,
|
||||||
|
host first, then the controller's `take`.
|
||||||
|
|
||||||
|
## Built, 2026-10-02
|
||||||
|
|
||||||
|
mesh-controller 201 and the pull request after it: the preview names it, and `take --yes <digest>`
|
||||||
|
acts on the preview that was read. Stays located until a take is read on an adopted machine — every
|
||||||
|
machine of this mesh is converged today, so the record's live row has not been run.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-02
|
||||||
|
|
||||||
|
Closed on the operator's decision of 2026-10-02 with the built and tested code live on every machine (mesh-controller 206, mesh-host 64), not on a take read on an adopted machine: every machine of this mesh is converged, so none holds a found thing to compare, and the record's live row — ADR 0163's last — will be read at the next real adoption rather than staged. Said here so nobody later believes that row was run.
|
||||||
|
|||||||
+22
-3
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-22
|
opened: 2026-09-22
|
||||||
located-in: []
|
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
|
||||||
fixed-by:
|
fixed-by: mesh-host 64 (genesis raises the forge as `gitea`, on the module's image digest, with the module's data directory at /data; a test holds the installer to the module's manifest)
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -48,3 +48,22 @@ network, or it is not a takeover.
|
|||||||
directory — so the module adopts it by the rule that already exists?
|
directory — so the module adopts it by the rule that already exists?
|
||||||
- Should something refuse to call a module the successor of a bootstrap service it cannot adopt?
|
- Should something refuse to call a module the successor of a bootstrap service it cannot adopt?
|
||||||
- Is the forge's own address better resolved than set, which is [issue 088](../088-the-forges-own-address-names-a-port-it-may-not-have/00-report.md)?
|
- Is the forge's own address better resolved than set, which is [issue 088](../088-the-forges-own-address-names-a-port-it-may-not-have/00-report.md)?
|
||||||
|
|
||||||
|
## Decided, 2026-10-01
|
||||||
|
|
||||||
|
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rule 7: genesis raises as the module declares. Building follows,
|
||||||
|
host first, then the controller's `take`.
|
||||||
|
|
||||||
|
## Built in part, 2026-10-02
|
||||||
|
|
||||||
|
mesh-host 64: genesis raises the forge under the module's container name (`gitea`), pinned to the
|
||||||
|
module's image digest, with the module's data directory mounted at `/data` — so the module finds it,
|
||||||
|
holds it, and a take compares equal images and the same data. A test holds the installer's constants
|
||||||
|
to the module's manifest where the catalogue is checked out beside it. The network is the difference
|
||||||
|
left: the bootstrap forge runs on the machine's network to reach the store on its loopback, the module
|
||||||
|
runs bridged and publishes its ports, and the take says so. Closing waits for group 9's genesis test —
|
||||||
|
a mesh raised, the module assigned, and the module found holding rather than raising a second forge.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-02
|
||||||
|
|
||||||
|
Closed on the operator's decision of 2026-10-02. Name, image and data directory align; the network does not — the bootstrap forge runs on the machine's network to reach the store on its loopback, the module runs bridged — and a take says so rather than hides it. Whether genesis should move the forge onto a bridge, and the test that raises a mesh and finds the module holding rather than raising a second forge, belong to group 9's genesis work and are not owed by this record any more.
|
||||||
|
|||||||
+9
-2
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-09-22
|
opened: 2026-09-22
|
||||||
located-in: [mesh-catalog, mesh-controller internal/catalogue]
|
located-in: [mesh-catalog, mesh-controller internal/catalogue]
|
||||||
fixed-by:
|
fixed-by: ADR 0104 — the route adapter module (mesh-catalog modules/route-adapter) writes each migrated route into the predecessor's proxy; it runs on the home server's migration
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -71,3 +71,10 @@ answered by an **adapter** that writes into the predecessor's own configuration.
|
|||||||
the predecessor's proxy keeps serving every name and keeps its certificates, while each migrated
|
the predecessor's proxy keeps serving every name and keeps its certificates, while each migrated
|
||||||
module's name is pointed at the mesh's container. The proxy is the last cutover again, and by then
|
module's name is pointed at the mesh's container. The proxy is the last cutover again, and by then
|
||||||
every route is one the mesh contributed.
|
every route is one the mesh contributed.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01
|
||||||
|
|
||||||
|
The adapter ADR 0104 decided exists and runs: `route-adapter` provides `route` on an adopted
|
||||||
|
machine by writing each migrated module's route where the predecessor's proxy reads it, and the
|
||||||
|
proxy itself is the last cutover. [ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md)
|
||||||
|
records the rest of what a take compares.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-23
|
opened: 2026-09-23
|
||||||
located-in: []
|
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
|
||||||
fixed-by:
|
fixed-by: mesh-controller (the pull request after 201: JudgeSettings, LeftOut), mesh-host 64 (left_out kept)
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -58,3 +58,17 @@ knowing the code.
|
|||||||
an operator to undo it without reading the source?
|
an operator to undo it without reading the source?
|
||||||
- Is there anything a node must never be pushed without, such that sending a partial declaration is
|
- Is there anything a node must never be pushed without, such that sending a partial declaration is
|
||||||
worse than sending none?
|
worse than sending none?
|
||||||
|
|
||||||
|
## Decided, 2026-10-01
|
||||||
|
|
||||||
|
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rule 6: judged where stored; an impossible statement costs a module. Building follows,
|
||||||
|
host first, then the controller's `take`.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-02
|
||||||
|
|
||||||
|
One judgement, in the catalogue, run where a setting is stored and where a machine is composed. Stored,
|
||||||
|
a setting that cannot compose with the module's current definition is refused naming the node, the
|
||||||
|
module, the layer and the key; a key that reaches nothing is refused there too. Composed, a definition
|
||||||
|
that moved under a stored setting leaves that module out of the machine's declaration — the envelope
|
||||||
|
names it, the host keeps what it holds and wrote for it, `plan` and `push` say it — and the machine is
|
||||||
|
told everything else. A stray setting no longer refuses the whole machine where it is read.
|
||||||
|
|||||||
+16
-3
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-23
|
opened: 2026-09-23
|
||||||
located-in: []
|
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
|
||||||
fixed-by:
|
fixed-by: mesh-host 63 (former targets removed, strays reported), mesh-controller 201/202 (strays shown)
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -75,3 +75,16 @@ found, and so would be kept for ever on purpose.
|
|||||||
module unassigned between the two declarations?
|
module unassigned between the two declarations?
|
||||||
- What reports this? Nothing on the machine currently answers "what is running here that the mesh
|
- What reports this? Nothing on the machine currently answers "what is running here that the mesh
|
||||||
did not ask for", which is the question that would have found this in seconds.
|
did not ask for", which is the question that would have found this in seconds.
|
||||||
|
|
||||||
|
## Decided, 2026-10-01
|
||||||
|
|
||||||
|
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rule 5: former targets are removed and strays reported. Building follows,
|
||||||
|
host first, then the controller's `take`.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-02
|
||||||
|
|
||||||
|
mesh-host 63: the host's record keeps a resource's former targets, removes a container or file it
|
||||||
|
wrote under a name the declaration no longer names, never what was found, and reports strays — what
|
||||||
|
runs on the machine that the mesh neither wrote nor holds. mesh-controller 201 and 202 show strays
|
||||||
|
on `node show` for an adopted and a converged machine alike; the live mesh reported four on the
|
||||||
|
control node the evening it rolled.
|
||||||
|
|||||||
+18
-3
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-23
|
opened: 2026-09-23
|
||||||
located-in: []
|
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
|
||||||
fixed-by:
|
fixed-by: mesh-host 63 (the kept original's difference), mesh-controller 201 (shown; a differing file refuses unless `--replace <path>`)
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -63,3 +63,18 @@ written.
|
|||||||
substitutes settings into content today.
|
substitutes settings into content today.
|
||||||
- Is the kept original enough of an answer, given nothing restores it and nothing points at it
|
- Is the kept original enough of an answer, given nothing restores it and nothing points at it
|
||||||
when the service starts behaving differently?
|
when the service starts behaving differently?
|
||||||
|
|
||||||
|
## Decided, 2026-10-01
|
||||||
|
|
||||||
|
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 2: the difference is shown and a differing file refuses. Building follows,
|
||||||
|
host first, then the controller's `take`.
|
||||||
|
|
||||||
|
## Built, 2026-10-02
|
||||||
|
|
||||||
|
mesh-host 63 reports the difference between the kept original and the declared content; mesh-controller
|
||||||
|
201 shows it in the preview and refuses a differing file unless `--replace <path>` names it, or the
|
||||||
|
module declares the file partially. Stays located until a take is read on an adopted machine.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-02
|
||||||
|
|
||||||
|
Closed on the operator's decision of 2026-10-02 with the built and tested code live on every machine (mesh-controller 206, mesh-host 64), not on a take read on an adopted machine: every machine of this mesh is converged, so none holds a found thing to compare, and the record's live row — ADR 0163's last — will be read at the next real adoption rather than staged. Said here so nobody later believes that row was run.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-23
|
opened: 2026-09-23
|
||||||
located-in: []
|
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
|
||||||
fixed-by:
|
fixed-by: mesh-host 63 (both images' creation dates), mesh-controller 201 (DOWNGRADE said; refused unless `--downgrade`)
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -60,3 +60,18 @@ expected rate.
|
|||||||
nothing answers the first.
|
nothing answers the first.
|
||||||
- Is a digest pin the right thing for a module that takes over an existing service at all, or
|
- Is a digest pin the right thing for a module that takes over an existing service at all, or
|
||||||
should a cutover be able to say *keep what is running* and record what that was?
|
should a cutover be able to say *keep what is running* and record what that was?
|
||||||
|
|
||||||
|
## Decided, 2026-10-01
|
||||||
|
|
||||||
|
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 2: the images are compared by age and a downgrade refuses. Building follows,
|
||||||
|
host first, then the controller's `take`.
|
||||||
|
|
||||||
|
## Built, 2026-10-02
|
||||||
|
|
||||||
|
mesh-host 63 reports the found image and both images' creation dates; mesh-controller 201 says
|
||||||
|
DOWNGRADE and refuses unless `--downgrade` is said. Stays located until a take is read on an adopted
|
||||||
|
machine.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-02
|
||||||
|
|
||||||
|
Closed on the operator's decision of 2026-10-02 with the built and tested code live on every machine (mesh-controller 206, mesh-host 64), not on a take read on an adopted machine: every machine of this mesh is converged, so none holds a found thing to compare, and the record's live row — ADR 0163's last — will be read at the next real adoption rather than staged. Said here so nobody later believes that row was run.
|
||||||
|
|||||||
+20
-3
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-23
|
opened: 2026-09-23
|
||||||
located-in: []
|
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
|
||||||
fixed-by:
|
fixed-by: mesh-controller 201 (`secret accept --provider` reaches a required secret), 206 (a module's secrets listed with origin; a minted one for found data refuses unless `--mint <name>`)
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -65,3 +65,20 @@ the module can only be installed fresh.
|
|||||||
Should it, so the dangerous case can be refused rather than discovered?
|
Should it, so the dangerous case can be refused rather than discovered?
|
||||||
- What is the reverse path: the mesh has minted one, the service ignored it, and the working value
|
- What is the reverse path: the mesh has minted one, the service ignored it, and the working value
|
||||||
is still on the machine. Nothing reconciles those.
|
is still on the machine. Nothing reconciles those.
|
||||||
|
|
||||||
|
## Decided, 2026-10-01
|
||||||
|
|
||||||
|
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 2 and 3: a minted secret for found data refuses; secret accept reaches required secrets. Building follows,
|
||||||
|
host first, then the controller's `take`.
|
||||||
|
|
||||||
|
## Built, 2026-10-02
|
||||||
|
|
||||||
|
`secret accept <node> <module> <name> --provider <node>` reaches a required secret (mesh-controller 201).
|
||||||
|
The pull request after it reads every secret a module holds on a machine with its origin, and a take
|
||||||
|
of a module whose data was found refuses a minted, unaccepted one — naming the accept that carries
|
||||||
|
the existing value in, or `--mint <name>` to let the service take the new one. Stays located until
|
||||||
|
a take is read on an adopted machine.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-02
|
||||||
|
|
||||||
|
Closed on the operator's decision of 2026-10-02 with the built and tested code live on every machine (mesh-controller 206, mesh-host 64), not on a take read on an adopted machine: every machine of this mesh is converged, so none holds a found thing to compare, and the record's live row — ADR 0163's last — will be read at the next real adoption rather than staged. Said here so nobody later believes that row was run.
|
||||||
|
|||||||
+19
-3
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-23
|
opened: 2026-09-23
|
||||||
located-in: []
|
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
|
||||||
fixed-by:
|
fixed-by: mesh-controller 201 (the neighbours on a found network are named), 206 (the per-machine `networks` setting), mesh-host 64 (the taken container joins the kept network)
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -61,3 +61,19 @@ exercise.
|
|||||||
learn to take a group atomically? Nothing takes more than one module at a time today.
|
learn to take a group atomically? Nothing takes more than one module at a time today.
|
||||||
- Does the same hole exist for anything else the predecessor's runtime resolves and the mesh's does
|
- Does the same hole exist for anything else the predecessor's runtime resolves and the mesh's does
|
||||||
not — a network alias, a `depends_on`, a name in a shared `/etc/hosts`?
|
not — a network alias, a `depends_on`, a name in a shared `/etc/hosts`?
|
||||||
|
|
||||||
|
## Decided, 2026-10-01
|
||||||
|
|
||||||
|
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 4: the neighbours are named; a found network may be kept by a setting. Building follows,
|
||||||
|
host first, then the controller's `take`.
|
||||||
|
|
||||||
|
## Built, 2026-10-02
|
||||||
|
|
||||||
|
The preview names every neighbour on a found network (mesh-controller 201). The pull request after it
|
||||||
|
adds the per-machine setting `networks` — a container id to the found networks it keeps — judged for an
|
||||||
|
adopted machine only, and mesh-host 64 has the taken container join each once it runs. Stays located
|
||||||
|
until a take is read on an adopted machine.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-02
|
||||||
|
|
||||||
|
Closed on the operator's decision of 2026-10-02 with the built and tested code live on every machine (mesh-controller 206, mesh-host 64), not on a take read on an adopted machine: every machine of this mesh is converged, so none holds a found thing to compare, and the record's live row — ADR 0163's last — will be read at the next real adoption rather than staged. Said here so nobody later believes that row was run.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-23
|
opened: 2026-09-23
|
||||||
located-in: []
|
located-in: [mesh-controller cmd/mesh-controller/network.go (the placing command), internal/inventory/migrations/0004-the-overlay.sql (one hub)]
|
||||||
fixed-by:
|
fixed-by: nothing to build — ADR 0161 rule 2; the second hub was already refused by name, and the private network's seat waits for ADR 0121's server and client modules
|
||||||
amended-design:
|
amended-design: [03-DESIGN/01-to-be/26-the-seats.md]
|
||||||
---
|
---
|
||||||
|
|
||||||
# 105 — The hub of the private network is a placement, not a seat
|
# 105 — The hub of the private network is a placement, not a seat
|
||||||
@@ -32,3 +32,16 @@ a node-scoped seat held by every node, which is true and not what was asked.
|
|||||||
- Does the per-node seat still say anything once the hub is a seat, or is it the interface's
|
- Does the per-node seat still say anything once the hub is a seat, or is it the interface's
|
||||||
presence restated?
|
presence restated?
|
||||||
- What else in the mesh is "exactly one" and recorded as a placement rather than a seat?
|
- What else in the mesh is "exactly one" and recorded as a placement rather than a seat?
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01
|
||||||
|
|
||||||
|
Read against the code: the store has kept one hub since the overlay's first migration (a unique
|
||||||
|
index), and `overlay place <node> --hub` refuses a second naming the first. What this report saw as
|
||||||
|
silent is not. [ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md), rule 2, answers the
|
||||||
|
question that remained: a singular fact about machines is a placement with a capacity of one,
|
||||||
|
refused by name and named in the listing — never a seat, because a seat is held by a module
|
||||||
|
assignment and the private network is the host's own until
|
||||||
|
[ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)'s
|
||||||
|
server and client modules exist. That seat stands, deferred with the split it needs.
|
||||||
|
|
||||||
|
*How it is checked:* the overlay command's test for a second hub, and the index.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-23
|
opened: 2026-09-23
|
||||||
located-in: []
|
located-in: [mesh-controller internal/catalogue/seats.go (the seed lacks mesh-vault), mesh-catalog modules/mesh-vault/module.json (claims nothing)]
|
||||||
fixed-by:
|
fixed-by: mesh-controller PR 192 (the seat row), mesh-catalog PR 205 (the claim; the vault's events renamed to its own)
|
||||||
amended-design:
|
amended-design: [03-DESIGN/01-to-be/26-the-seats.md]
|
||||||
---
|
---
|
||||||
|
|
||||||
# 106 — The vault claims no seat, so nothing refuses a second one
|
# 106 — The vault claims no seat, so nothing refuses a second one
|
||||||
@@ -31,3 +31,19 @@ others were missed the same way — every provider added after 0079.
|
|||||||
- A mesh-scoped seat `mesh-vault`, by the 0079 convention — is there any reason not to?
|
- A mesh-scoped seat `mesh-vault`, by the 0079 convention — is there any reason not to?
|
||||||
- Should a provider of a mesh-scoped provision be required to claim a seat, or say explicitly that
|
- Should a provider of a mesh-scoped provision be required to claim a seat, or say explicitly that
|
||||||
more than one is allowed, so the omission cannot recur?
|
more than one is allowed, so the omission cannot recur?
|
||||||
|
|
||||||
|
## Decided, 2026-10-01
|
||||||
|
|
||||||
|
[ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md), rule 1: `mesh-vault` joins the mesh's
|
||||||
|
own set, mesh-scoped, delivering `secret`, and the vault claims it; a second provider is a second
|
||||||
|
claimant, refused by name. The record also answers the second question: a provision the mesh's own
|
||||||
|
code dereferences by name gets a seat, every other mesh-scoped provision may have several providers.
|
||||||
|
Design 26's *reserved* for `secret` named an effect no rule produced; corrected there.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01
|
||||||
|
|
||||||
|
`seats` on the live mesh lists `mesh-vault` at mesh scope, delivering `secret`, held by the vault on
|
||||||
|
the control node. A second provider of `secret` is now a second claimant and refused by name
|
||||||
|
(`CanHold`'s test). Found on the way: the vault's definition could not be rebuilt at all — it emitted
|
||||||
|
`secret.provisioned` and the like, which the builder reads as another module's events — so the events
|
||||||
|
are now the vault's own, `provisioned`, `rotated`, `deprovisioned`.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-09-25
|
opened: 2026-09-25
|
||||||
located-in: [mesh-catalog modules, mesh-controller internal/catalogue]
|
located-in: [mesh-catalog modules, mesh-controller internal/catalogue]
|
||||||
fixed-by:
|
fixed-by: mesh-catalog PR 193 (the conversion), mesh-controller PR 170 (TestPlacedDirectoriesKeepTheirPaths, which proves it moved nothing); the placed-directory mechanism itself predates this in mesh-controller internal/catalogue/dir_into.go
|
||||||
amended-design:
|
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 119 — A module definition decides where its files live on the machine
|
# 119 — A module definition decides where its files live on the machine
|
||||||
@@ -133,3 +133,30 @@ not by any check.
|
|||||||
- What identifies an assignment, if a module may be assigned to one node more than once?
|
- What identifies an assignment, if a module may be assigned to one node more than once?
|
||||||
- What would the contributions file carry instead of host paths, so a provider needs no
|
- What would the contributions file carry instead of host paths, so a provider needs no
|
||||||
identical-path mount?
|
identical-path mount?
|
||||||
|
|
||||||
|
## Resolved, 2026-09-30 — the module's half; the mesh's half is issue 174
|
||||||
|
|
||||||
|
**A definition no longer decides where its own data lives.** Twenty-eight definitions that named their
|
||||||
|
data directories now place them: the module's root as `place: "."`, a sub-directory by its id, and every
|
||||||
|
host-side reference — bindings, secrets, own secrets, grants, receives, file paths, mounts, env-files —
|
||||||
|
as `${dir:<id>}`. Twenty-eight others had already been written that way. Five directories whose id is
|
||||||
|
not their last segment keep their path as a placement, which is the exception the design allows and
|
||||||
|
the reason nothing else has to move for them.
|
||||||
|
|
||||||
|
**Nothing moved, and a test says so.** The controller's `TestPlacedDirectoriesKeepTheirPaths` takes the
|
||||||
|
catalogue before and after, resolves every converted definition on the default root with the
|
||||||
|
controller's own rule, and compares it whole with the definition before it: identical for all
|
||||||
|
twenty-eight. So the retirement this record said was a data migration turned out not to be one, on
|
||||||
|
one condition — a node's default root is where the data already is, and every node's is — and the
|
||||||
|
machines see no change. A node that sets another root is the case this does not cover, and it does
|
||||||
|
not exist.
|
||||||
|
|
||||||
|
**What remains is not this record's.** The 232 host paths still in the catalogue are where the mesh
|
||||||
|
writes what it makes for a module, under `/var/lib/mesh/<module>`; design 27 says the mesh places
|
||||||
|
those itself, and it does not yet. That is [issue 174](../174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md). *Placed since later the same day: `place: "mesh"` — issue 174 is resolved.*
|
||||||
|
The defects this record listed under *where that has already gone wrong* are unchanged by this and
|
||||||
|
stay in 174's scope where they concern the mesh's files; the operator's shared data stays an access
|
||||||
|
([ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md)).
|
||||||
|
|
||||||
|
The manifest change lands with the catalogue's next merge; the rollout is a rebuild that changes no
|
||||||
|
machine, checked by comparing each machine's plan before and after.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-09-26
|
opened: 2026-09-26
|
||||||
located-in: [mesh-controller internal/catalogue/declaration.go, mesh-catalog modules/keycloak, mesh-catalog modules/minio, mesh-catalog modules/nextcloud]
|
located-in: [mesh-controller internal/catalogue/declaration.go, mesh-catalog modules/keycloak, mesh-catalog modules/minio, mesh-catalog modules/nextcloud]
|
||||||
fixed-by:
|
fixed-by: mesh-controller PR 149 (a module is told the name it is served under), PR 169 (an operator's value, a context on the seat); mesh-catalog PR 188; ADR 0155
|
||||||
amended-design:
|
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 122 — A module cannot ask for its own public name, so three manifests wrote this mesh's names into the catalogue
|
# 122 — A module cannot ask for its own public name, so three manifests wrote this mesh's names into the catalogue
|
||||||
@@ -114,3 +114,27 @@ particular to one installation, and also has nowhere to live but the definition.
|
|||||||
ADR 0112 would put it), and if so what reads it — the provisioner, or the module's own values?
|
ADR 0112 would put it), and if so what reads it — the provisioner, or the module's own values?
|
||||||
- What check would notice the next one? A definition naming a public domain is detectable in the
|
- What check would notice the next one? A definition naming a public domain is detectable in the
|
||||||
shape of the value, which is more than nothing, and less than a rule.
|
shape of the value, which is more than nothing, and less than a rule.
|
||||||
|
|
||||||
|
## Resolved, 2026-09-30
|
||||||
|
|
||||||
|
The open questions, answered in order. **A module names what it will be reached at** through the
|
||||||
|
binding of the route it contributes: `${bound:route:name}`, or `:name-<local>` for several
|
||||||
|
contributions, is the composed public name; `:internal-name` the private one (controller PR 149). The
|
||||||
|
identity provider, the object store's console and the automation tool's webhook now read it there.
|
||||||
|
**The scheme is the module's**, because it is true of the route the proxy terminates and every instance
|
||||||
|
wrote `https://` in front of the name. **An adopted resource's name is a setting** on the assignment;
|
||||||
|
so is every other operator's value — a mail domain, a site name, the address a proxy forwards from —
|
||||||
|
as `${setting:<key>}` in the file the software reads
|
||||||
|
([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)).
|
||||||
|
**The check that notices the next one** is the shape of the value, as this record guessed it would be,
|
||||||
|
and it is more than nothing: it found forty-two, and the catalogue passes it now
|
||||||
|
([issue 134](../134-a-definition-may-still-name-the-mesh/00-report.md)).
|
||||||
|
|
||||||
|
**What the rollout cost, 2026-09-30 evening.** The site module's rename from its domain to `website`
|
||||||
|
was a new module to the mesh, and two things the old assignment carried by name were lost: the
|
||||||
|
container still named the old network, and a port setting on the old assignment had hidden that
|
||||||
|
`listens` said one port while the container published another. The site answered 502 for about
|
||||||
|
twenty minutes across two one-line fixes (mesh-catalog PRs 190, 191). A module's rename is an
|
||||||
|
unassign and an assign, and everything the assignment held — settings, ports, its directory — is the
|
||||||
|
new module's to get again; the mesh says nothing about that today. The mail module's settings turned
|
||||||
|
out to reach every fact it contributes, which is [issue 173](../173-a-modules-settings-reach-every-fact-it-contributes/00-report.md).
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-09-26
|
opened: 2026-09-26
|
||||||
located-in: [hq 00-META/glossary.md, hq 02-DECISIONS/0075-two-stores-and-which-provides-what.md, mesh-controller internal/catalogue/seats.go, mesh-catalog modules/distribution]
|
located-in: [hq 00-META/glossary.md, hq 02-DECISIONS/0075-two-stores-and-which-provides-what.md, mesh-controller internal/catalogue/seats.go, mesh-catalog modules/distribution]
|
||||||
fixed-by:
|
fixed-by: ADR 0156; mesh-controller migration 0048 (feat/the-artifact-store-seat-is-named-for-its-scope); mesh-catalog modules/distribution
|
||||||
amended-design:
|
amended-design: 03-DESIGN/01-to-be/26-the-seats.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 123 — The image registry is named after a role, and *artifact* is defined as one format
|
# 123 — The image registry is named after a role, and *artifact* is defined as one format
|
||||||
@@ -72,3 +72,15 @@ adopted, rather than on protocols.
|
|||||||
exercise that leaves the code disagreeing?
|
exercise that leaves the code disagreeing?
|
||||||
- What check would keep the glossary honest — a definition tested against the kinds a definition may
|
- What check would keep the glossary honest — a definition tested against the kinds a definition may
|
||||||
actually declare, rather than restated by hand?
|
actually declare, rather than restated by hand?
|
||||||
|
|
||||||
|
## Resolved, 2026-09-30
|
||||||
|
|
||||||
|
Read from what the store serves rather than from what it was called: both shapes of a kept reference
|
||||||
|
— an image and an archive's blob — go to the same registry by digest, which is exactly the provision
|
||||||
|
ADR 0075 defined. So the word was wrong and the seat's name was odd, and the provision was right.
|
||||||
|
[ADR 0156](../../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md):
|
||||||
|
*artifact* means what a build produces, of any of the four kinds; the seat is `mesh-artifact-store`
|
||||||
|
with the old name as its alias (one migration, ADR 0122's mechanism); the provision keeps its name.
|
||||||
|
The two-implementations question stays as 0075 answered it, with the day to retire the second server
|
||||||
|
named. The mechanical check the report asked for is the alias test and the glossary naming the same
|
||||||
|
four kinds as design 18's table.
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-26
|
opened: 2026-09-26
|
||||||
located-in: [mesh-host internal/apply]
|
located-in: [mesh-host internal/apply]
|
||||||
|
fixed-by: mesh-host 63 (every written field compared), mesh-controller 201 (build says the policy)
|
||||||
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
# 126 — a volume path is not in the spec comparison, and a roll-out raced a data move
|
# 126 — a volume path is not in the spec comparison, and a roll-out raced a data move
|
||||||
@@ -45,3 +47,14 @@ Instant renames both ways broke the circular dependency (forge needed for builds
|
|||||||
builds needed for the push, push needed for the forge): data back to the old path,
|
builds needed for the push, push needed for the forge): data back to the old path,
|
||||||
old-spec forge started, artifacts rebuilt, data renamed forward, push. Nothing lost;
|
old-spec forge started, artifacts rebuilt, data renamed forward, push. Nothing lost;
|
||||||
the install-page junk was discarded twice.
|
the install-page junk was discarded twice.
|
||||||
|
|
||||||
|
## Decided, 2026-10-01
|
||||||
|
|
||||||
|
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 5 and 7: every field compared; build says the policy. Building follows,
|
||||||
|
host first, then the controller's `take`.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-02
|
||||||
|
|
||||||
|
mesh-host 63: every field the host writes is compared before a container is called current, volumes
|
||||||
|
and paths included. mesh-controller 201: `build` and the take-in say when a module's policy rolls a
|
||||||
|
result out at once; under ADR 0162 the plan says it too.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-28
|
opened: 2026-09-28
|
||||||
located-in: [mesh-catalog, mesh-controller internal/catalogue]
|
located-in: [mesh-catalog, mesh-controller internal/catalogue]
|
||||||
fixed-by:
|
fixed-by: mesh-controller PR 169 (the check, module check, the catalogue-wide test); mesh-catalog PR 188 (the catalogue that passes it); ADR 0155
|
||||||
amended-design:
|
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 134 — A definition may still name the mesh, and the check that would say so does not exist
|
# 134 — A definition may still name the mesh, and the check that would say so does not exist
|
||||||
@@ -64,3 +64,17 @@ that.
|
|||||||
hostnames above are the first real cases.
|
hostnames above are the first real cases.
|
||||||
- Should a build context name a repository on the git seat rather than by URL, and if so, what does
|
- Should a build context name a repository on the git seat rather than by URL, and if so, what does
|
||||||
that mean for a context in *another* mesh's forge?
|
that mean for a context in *another* mesh's forge?
|
||||||
|
|
||||||
|
## Resolved, 2026-09-30
|
||||||
|
|
||||||
|
The check exists: `InstallationProblems`, run by `module check` and by a catalogue-wide test. Run over
|
||||||
|
the 77 definitions it found 42 values, not 15 — the by-hand count had missed a second name one
|
||||||
|
character after the first on the same line, which is the kind of thing a check is for. The three open
|
||||||
|
questions: **a domain in a `why` string does not break the rule**, prose is not judged, and the eight
|
||||||
|
were rewritten anyway because this catalogue is public; **a service's public name is the name the mesh
|
||||||
|
composes for its route**, read through the route's binding, and an operator's own value is a setting;
|
||||||
|
**a build context names a repository on the git seat**, `seat: git` with the path, and a context in
|
||||||
|
another mesh's forge stays a URL, which the check reports and `names-on-purpose` would declare. The
|
||||||
|
seven values that remain are declared with their reason — four applications built outside the mesh —
|
||||||
|
and are the list that shrinks ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)).
|
||||||
|
Registration does not refuse yet; it will when the list has been empty for a release. *2026-09-30, later the same day:* it refuses — the list was empty the day the check landed, and the operator asked for it (mesh-controller PR 175); `module add` and a build's result are refused in the check's words, with the way out, and the build stays recorded.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-28
|
opened: 2026-09-28
|
||||||
located-in: [mesh-controller internal/catalogue, mesh-catalog]
|
located-in: [mesh-host internal/profile/detectors.go (no detector for the network manager), mesh-host cmd/mesh-host (the profile is detected at enrolment only), mesh-controller internal/link (a report carries no profile), mesh-catalog modules/networkmanager, systemd-networkd, dhcpcd (declare no capability of their own)]
|
||||||
fixed-by:
|
fixed-by: mesh-controller PR 192 (a report carries the profile), mesh-host PR 61 (uplink-<manager> detected and reported with every apply), mesh-catalog PR 206 (each holder declares its own)
|
||||||
amended-design:
|
amended-design: [03-DESIGN/01-to-be/26-the-seats.md]
|
||||||
---
|
---
|
||||||
|
|
||||||
# 138 — Two modules claim one seat and are not interchangeable, and nothing says so
|
# 138 — Two modules claim one seat and are not interchangeable, and nothing says so
|
||||||
@@ -54,3 +54,25 @@ able to switch the manager, which ADR 0117 refuses for a reason that has not cha
|
|||||||
alternative is one module that speaks whichever dialect the machine needs, chosen from the report.
|
alternative is one module that speaks whichever dialect the machine needs, chosen from the report.
|
||||||
- What should happen on a machine that switches manager afterwards? The seat would then be held by the
|
- What should happen on a machine that switches manager afterwards? The seat would then be held by the
|
||||||
wrong module, and the machine is the only place that knows.
|
wrong module, and the machine is the only place that knows.
|
||||||
|
|
||||||
|
## Decided, 2026-10-01
|
||||||
|
|
||||||
|
[ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md), rule 3: the host's profile gains one
|
||||||
|
capability per network manager found active, each holder declares its own, and the existing
|
||||||
|
capability refusal does the rest, naming it. The profile is detected again by every apply and
|
||||||
|
travels in the report, so a machine that switches managers is refused at its next push. The uplink
|
||||||
|
stays one seat; the capability picks the dialect. Order of building: controller (a report may carry
|
||||||
|
a profile), host, then the three definitions.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01
|
||||||
|
|
||||||
|
Every machine now reports which network manager it runs — the control node `uplink-systemd-networkd`,
|
||||||
|
the laptop and the workstation `uplink-networkmanager`, the home server both `uplink-dhcpcd` and
|
||||||
|
`uplink-networkmanager`, which is its truth — renewed with every report, and each holder declares the
|
||||||
|
capability it needs, so the wrong holder is refused on assignment with the capability named. The uplink
|
||||||
|
stays one seat; the capability picks the dialect. A machine that switches managers is a machine whose
|
||||||
|
holder lacks a capability at its next push.
|
||||||
|
|
||||||
|
*How it is checked:* the host's detector test per manager; the controller's test that a report's
|
||||||
|
profile replaces the enrolled one; the capability refusal's existing tests; live, `node show <machine>`
|
||||||
|
lists `uplink-<manager>` for each.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-09-28
|
opened: 2026-09-28
|
||||||
located-in:
|
located-in:
|
||||||
- mesh-controller internal/catalogue/filtering.go
|
- mesh-controller internal/catalogue/filtering.go
|
||||||
- mesh-host internal/apply
|
- mesh-host internal/apply
|
||||||
fixed-by:
|
fixed-by: ADR 0140 — mesh-controller (the filter around outward links; no network ranges anywhere)
|
||||||
amended-design: 03-DESIGN/01-to-be/08-connectivity.md
|
amended-design: 03-DESIGN/01-to-be/08-connectivity.md
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -86,3 +86,11 @@ supersedes both 0137 and the first attempt at answering this.
|
|||||||
runtime, or left as the one constant?
|
runtime, or left as the one constant?
|
||||||
- Should the preview say which of a machine's networks are the mesh's and which are not, so a range
|
- Should the preview say which of a machine's networks are the mesh's and which are not, so a range
|
||||||
that exists to protect a leftover is visible as such?
|
that exists to protect a leftover is visible as such?
|
||||||
|
|
||||||
|
## Resolved, 2026-10-02
|
||||||
|
|
||||||
|
By [ADR 0140](../../02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md), built and
|
||||||
|
live since 2026-09-29: the forward chain constrains what arrives on the machine's outward links and
|
||||||
|
says nothing about networks, so there is no list to derive and nothing for a preview to tell apart.
|
||||||
|
Read into [ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md),
|
||||||
|
rule 5, which closes it.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-09-29
|
opened: 2026-09-29
|
||||||
located-in:
|
located-in:
|
||||||
- mesh-host internal/apply/opening.go (retireFirewall)
|
- mesh-host internal/apply/opening.go (retireFirewall)
|
||||||
- mesh-host internal/apply/apply.go (the condition it is called under)
|
- mesh-host internal/apply/apply.go (the condition it is called under)
|
||||||
fixed-by:
|
fixed-by: mesh-host 67 (retire on every converged apply; found-inactive apart from disabled-by-mesh; a skipped step said), mesh-controller 211 (the found firewall's state on node show)
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -100,3 +100,21 @@ harmless, but the mesh's belief about which firewall is in force has been wrong
|
|||||||
so". Should it?
|
so". Should it?
|
||||||
- Why do the host's own detail lines not reach the journal? Everything it decided during the flip is
|
- Why do the host's own detail lines not reach the journal? Everything it decided during the flip is
|
||||||
unrecoverable, which is why this account has candidates instead of a cause.
|
unrecoverable, which is why this account has candidates instead of a cause.
|
||||||
|
|
||||||
|
## Decided, 2026-10-02
|
||||||
|
|
||||||
|
[ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), rule 1:
|
||||||
|
convergence is a state the host keeps — the found firewall active again is retired again and said, a
|
||||||
|
reconcile that finds it inactive records *found so* and never *done by the mesh*, and a step skipped
|
||||||
|
after a failed apply is said. Built in mesh-host on `feat/one-thing-filters-a-converged-machine`; the
|
||||||
|
record of both machines of this mesh is corrected by the first report under it.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-02
|
||||||
|
|
||||||
|
mesh-host 67 and mesh-controller 211, live on every machine at 10:10Z. The step now runs on every
|
||||||
|
converged apply and says what it did; a found firewall enabled again is retired again. The record's
|
||||||
|
one inherited lie stands as history: on the control node the machine's own record already said the
|
||||||
|
mesh had disabled the firewall, and the host trusts its record, so `node show` says "retired by the
|
||||||
|
mesh" there. From this build on, a reconcile that finds the firewall inactive records *found inactive*
|
||||||
|
and never the other thing. Whether the flip's step took on 2026-09-29 is not recoverable and is not
|
||||||
|
owed by this record any more.
|
||||||
|
|||||||
+23
-2
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-09-29
|
opened: 2026-09-29
|
||||||
located-in:
|
located-in:
|
||||||
- mesh-host internal/apply/opening.go
|
- mesh-host internal/apply/opening.go
|
||||||
- mesh-controller cmd/mesh-controller (the converge preview)
|
- mesh-controller cmd/mesh-controller (the converge preview)
|
||||||
fixed-by:
|
fixed-by: mesh-host 67 (every refusing table and legacy chain classified with an owner; the runtime's user chain is other), mesh-controller 211 (kept, shown on node show, named by status, previewed with fates)
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -82,3 +82,24 @@ everything reached from within.
|
|||||||
- Is the bus and the registry being reachable from anywhere still what the mesh wants on a machine that
|
- Is the bus and the registry being reachable from anywhere still what the mesh wants on a machine that
|
||||||
faces the internet? The design says yes, for enrolment. It deserves asking on its own rather than
|
faces the internet? The design says yes, for enrolment. It deserves asking on its own rather than
|
||||||
being answered by a leftover.
|
being answered by a leftover.
|
||||||
|
|
||||||
|
## Decided, 2026-10-02
|
||||||
|
|
||||||
|
[ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), rules 2 and
|
||||||
|
3: the host reports every table and legacy chain that refuses, with an owner, and the runtime's user
|
||||||
|
chain's refusals as *other*; `node show`, `status` and the converge preview say it. Built on
|
||||||
|
`feat/one-thing-filters-a-converged-machine` in mesh-host and mesh-controller. On 2026-10-02 the home
|
||||||
|
server still carries the predecessor's chain in its legacy filter; the record's live row is reading it
|
||||||
|
there.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-02
|
||||||
|
|
||||||
|
mesh-host 67 and mesh-controller 211, live at 10:10Z. The live row of
|
||||||
|
[ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md) was read the
|
||||||
|
same hour: the home server's record names the predecessor's chain in the legacy filter's user chain
|
||||||
|
as *other*, with what it refuses, beside two chains a retired front end left in the IPv6 legacy filter;
|
||||||
|
the control node's record names the same two leftovers; the laptop and the workstation read *the mesh
|
||||||
|
alone*. `status` names both machines and is not well until the operator removes what the mesh did not
|
||||||
|
write. The allowance the predecessor's chain carried is
|
||||||
|
[issue 145](../145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)'s,
|
||||||
|
and that record is not closed by this one.
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-29
|
opened: 2026-09-29
|
||||||
located-in:
|
located-in:
|
||||||
- mesh-controller internal/catalogue/dir_into.go (dirsFor: a stated path or <data root>/<module>/<id>, nothing else)
|
- mesh-controller internal/catalogue/dir_into.go (dirsFor: a stated path or <data root>/<module>/<id>, nothing else)
|
||||||
- mesh-controller (accesses: the path is the manifest's literal)
|
- mesh-controller (accesses: the path is the manifest's literal)
|
||||||
fixed-by:
|
fixed-by: mesh-controller PR 176 (`places` and `accesses` on an assignment, `${access:<id>}`, an owner the data already has); mesh-catalog PR 198 (ten definitions name their accesses by id)
|
||||||
amended-design:
|
amended-design: [03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md, 03-DESIGN/01-to-be/18-building-a-module.md]
|
||||||
---
|
---
|
||||||
|
|
||||||
# 153 — An adopted machine's data cannot be placed where it is
|
# 153 — An adopted machine's data cannot be placed where it is
|
||||||
@@ -55,3 +55,25 @@ The two assignment halves 0112 decided: a setting that places a declared directo
|
|||||||
path on this node, and a setting that says where an access's data is — both validated like
|
path on this node, and a setting that says where an access's data is — both validated like
|
||||||
`endpoints` (unknown ids refused), and an access placed by the assignment still never created,
|
`endpoints` (unknown ids refused), and an access placed by the assignment still never created,
|
||||||
chowned or removed.
|
chowned or removed.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01
|
||||||
|
|
||||||
|
The two assignment halves ADR 0112 decided exist. On an assignment's settings, `places` puts a
|
||||||
|
declared directory (by id) at a path on this node, with an owner where the data already has one —
|
||||||
|
`{"config": "/where/it/is", "data": {"path": "…", "owner": "1001:2000"}}` — and `accesses` says where
|
||||||
|
the operator's data is, by the access's id. Both are validated the way `endpoints` is: an id the
|
||||||
|
definition does not declare is refused, naming what it does declare; a relative path and a
|
||||||
|
non-numeric owner are refused; an access nothing places and whose definition carries no path is
|
||||||
|
refused with the setting to write, rather than mounted as nothing. A placed directory is still the
|
||||||
|
mesh's — created, owned as said, removed when empty and undeclared. A placed access is still the
|
||||||
|
operator's — mounted, never created, owned or removed.
|
||||||
|
|
||||||
|
An access now has an **id**, and the definition's mounts name it as `${access:<id>}`, so a placement
|
||||||
|
moves the mount with it. Ten catalogue definitions were given ids; each keeps its path as the default
|
||||||
|
an assignment may replace, so the machine that said nothing received exactly the paths it had before
|
||||||
|
(the path-preservation proof, extended to accesses). That default is still a host path in a
|
||||||
|
definition, tolerated as the transition: the media modules on the control node hold it until their
|
||||||
|
assignments say where the data is, and then the defaults go.
|
||||||
|
|
||||||
|
What the home server's assignments say next is the operator's: per media module, `places` for the
|
||||||
|
configuration on the second disk and `accesses` for the pool, with the owner the predecessor ran as.
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-09-30
|
||||||
|
located-in:
|
||||||
|
- mesh-controller internal/inventory/secrets.go (SecretFor mints a pair credential nobody accepted)
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 164 — A credential that must be accepted is minted anyway
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
Provisioning ace's modules. Several providers hold exactly one credential they did not get from the
|
||||||
|
mesh and cannot take one from it: a Servarr app's API key (sonarr, radarr, lidarr), jackett's API key,
|
||||||
|
plex's X-Plex-Token, nzbget's ControlPassword, qBittorrent's WebUI password. Their consumers' pair
|
||||||
|
credential must be **accepted** by the operator (ADR 0092). Until it is, `SecretFor` mints a random
|
||||||
|
value, seals it to both ends, and reports nothing: the value can never work.
|
||||||
|
|
||||||
|
Every consumer therefore had to learn to detect it — try the credential against the provider first,
|
||||||
|
refuse a value the provider rejects, print the `secret accept` command — six write-in steps, one probe
|
||||||
|
each (ombi, home-assistant, and the four download-stack consumers). qBittorrent bans an address after
|
||||||
|
five failed logins, so a consumer retrying a minted value locks itself out.
|
||||||
|
|
||||||
|
## What would be right
|
||||||
|
|
||||||
|
A provision (or a provider's `serves`) can declare its pair credential **accepted-only**. The plan then
|
||||||
|
refuses the pair — naming the accept command — instead of minting, and a consumer is never handed a
|
||||||
|
value the mesh knows cannot work.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-09-30
|
||||||
|
located-in:
|
||||||
|
- mesh-controller internal/inventory/secrets.go (AcceptSecretForPair is per consumer)
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 165 — One accepted value must be accepted once per consumer
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
On ace, jackett's API key is the pair credential for sonarr, radarr, lidarr and bookshelf; sonarr's is
|
||||||
|
the credential for ombi, bazarr and home-assistant. It is **one value**, owned by the provider — yet
|
||||||
|
`secret accept` is per pair, so ace's download stack alone needs 12 accepts of 3 values, and rotating
|
||||||
|
a provider's key means finding and re-accepting every pair. Missing one leaves that consumer on a
|
||||||
|
stale (or minted, 164) value.
|
||||||
|
|
||||||
|
## What would be right
|
||||||
|
|
||||||
|
A provider-level accept: "this provider's credential for `<provision>` is X" — delivered to every
|
||||||
|
consumer pair, current and future, and rotated in one place. Pairs whose credential is genuinely per
|
||||||
|
consumer (postgres, keycloak, mosquitto, influxdb — minted and created by a provisioner) are unaffected.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-09-30
|
||||||
|
located-in:
|
||||||
|
- mesh-controller internal/catalogue (requires is a list of hard requirements)
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 166 — A requirement cannot be optional
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
Making every dependency on ace a provision turned soft dependencies into hard ones. grafana now
|
||||||
|
requires `influxdb-api` (a data source), ombi requires `sonarr-api`, `radarr-api` and `lidarr-api`,
|
||||||
|
home-assistant requires the Servarr APIs and `mqtt-topic`. Each is optional to the software — grafana
|
||||||
|
runs without a data source, ombi without lidarr — but a mesh without influxdb cannot assign grafana at
|
||||||
|
all, and a mesh without lidarr cannot run ombi.
|
||||||
|
|
||||||
|
## What would be right
|
||||||
|
|
||||||
|
A requirement a module can run without: resolved and bound when a provider exists, absent (with its
|
||||||
|
`${bound:…}` placeholders refused or defaulted explicitly, never rendered empty) when none does — so
|
||||||
|
the module description stays true on every mesh.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-09-30
|
||||||
|
located-in:
|
||||||
|
- mesh-catalog (each module builds from its own directory, ADR 0069)
|
||||||
|
- mesh-sdk
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 167 — Code several modules share has no home
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
The download-stack write-in step (register download clients and torznab indexers through the Servarr
|
||||||
|
API) is identical for sonarr, radarr, lidarr and bookshelf. Because a module builds from its own
|
||||||
|
directory, it now exists as four byte-identical copies under `modules/<m>/downloads/`, kept honest by a
|
||||||
|
test that fails when one differs. The same shape repeats: an MQTT probe copied into two modules, and a
|
||||||
|
"write the provider into the app through its API, idempotently, refuse a minted value" step in ombi,
|
||||||
|
home-assistant, nodered, tautulli and the four downloaders.
|
||||||
|
|
||||||
|
## What would be right
|
||||||
|
|
||||||
|
A home for shared module code the builder can use — an sdk helper (a write-in step harness: read
|
||||||
|
bindings and pair credentials, probe the provider, diff, write, report) or a shared package the
|
||||||
|
catalogue builds once — so a fix lands in one place.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-09-30
|
||||||
|
located-in:
|
||||||
|
- mesh-controller internal/catalogue/settings.go (settle: every key but `ports` merges into every mergeable file and every contribution)
|
||||||
|
- mesh-controller internal/catalogue/declaration.go (a provider's settings are laid over what it serves)
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 168 — A setting reaches every file and every contribution
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
Settings merge key by key into **every** `"merge": "json"` file of a module **and** every contribution
|
||||||
|
it makes; a provider's settings are also laid over what it serves. Seen on ace:
|
||||||
|
|
||||||
|
- searxng's `endpoints` and a route `label` land in searxng's own `settings.yml`; nodered's
|
||||||
|
`timeZone` and `mqtt` keys land in mosquitto's grants file; keycloak's `issuer` lands in its
|
||||||
|
`postgres-database` and `route` contributions.
|
||||||
|
- every consumer's `plex-api` binding carries plex's `endpoints` and `expose` settings — and a provider
|
||||||
|
setting named `port` would silently redirect every consumer.
|
||||||
|
- a module cannot have two configurable files: searxng's sidecar config had to stop being mergeable
|
||||||
|
so searxng's keys would not reach it.
|
||||||
|
|
||||||
|
Harmless today only because every receiver happens to ignore unknown keys.
|
||||||
|
|
||||||
|
## What would be right
|
||||||
|
|
||||||
|
A setting is aimed: at a file (by resource id), at a contribution (by requirement), or at what the
|
||||||
|
module serves — declared settable by the module (ADR 0046 already says settings drive "the fields the
|
||||||
|
manifest marks") — and an unaimed key is refused like any unknown setting.
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-09-30
|
||||||
|
located-in:
|
||||||
|
- mesh-catalog (no module shares a path over the network)
|
||||||
|
- hq 02-DECISIONS (a file-share seat, per ADR 0126, is a module's own to define)
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 169 — A machine shares its files, and the mesh does not know
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
ace serves the operator's media library to the home network with two host services no module
|
||||||
|
declares and HAL never managed either:
|
||||||
|
|
||||||
|
```
|
||||||
|
/etc/exports: /storage/media 192.168.1.0/24(rw,sync,root_squash,…) nfs-server active, :2049
|
||||||
|
/etc/samba/smb.conf: [media] path = /storage/media/ valid users = media smb active, :139/:445
|
||||||
|
```
|
||||||
|
|
||||||
|
Two LAN clients were connected at survey (2026-09-30). The library itself is operator data
|
||||||
|
(ADR 0051: ~40 TB on ZFS, the mesh owns nothing about it — [issue 153](../153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)
|
||||||
|
is about modules reaching it in place).
|
||||||
|
|
||||||
|
Under the mesh as it stands, this arrangement has no expression and one failure mode:
|
||||||
|
|
||||||
|
- **Nothing declares the listens.** At `converge ace` the filter is the sum of what modules listen
|
||||||
|
on (ADR 0045); 2049 and 445 are nobody's, so the shares close — silently, for the two clients
|
||||||
|
that mount them.
|
||||||
|
- **Nothing owns the configuration.** `/etc/exports` and `smb.conf` are hand-written files on one
|
||||||
|
machine; a second machine sharing a directory would be written by hand again.
|
||||||
|
- **Nothing can consume it.** A module on another node that wanted the library (a player, an
|
||||||
|
indexer, a backup) has no `requires` to state and no binding to read; it would mount by a
|
||||||
|
hand-typed host and path.
|
||||||
|
- The clients are LAN devices, so this also meets [issue 154](../154-a-machines-own-network-is-not-a-reach/00-report.md)
|
||||||
|
(no reach for the machine's own network).
|
||||||
|
|
||||||
|
## The proposal (the operator's, 2026-09-30, settled after two rounds)
|
||||||
|
|
||||||
|
**Two module-defined seats, one per protocol, because NFS and SMB share an intent and not a
|
||||||
|
contract.** A seat in the mesh's sense is a contract — what it accepts, emits and serves, and the
|
||||||
|
tools its holder must answer (ADR 0126, 0132) — and lined up, the two share almost none of it:
|
||||||
|
|
||||||
|
| | `nfs-share` | `smb-share` |
|
||||||
|
|---|---|---|
|
||||||
|
| serves | export path(s); the client ranges allowed (`sec=sys` authorises by address) | share name(s), path |
|
||||||
|
| pair credential | none | a user and password per consumer |
|
||||||
|
| consumer's mount | `at:/path` | `//at/share` with credentials |
|
||||||
|
| holder's tools | export / unexport a path for a range | add / remove a share, create a user |
|
||||||
|
|
||||||
|
One `file-share` seat would be the union with every field optional — a consumer could bind it and
|
||||||
|
still not know how to mount what it got (the emptiness ADR 0129 warns against). "Export a path to
|
||||||
|
the network" is a category, and the mesh needs no seat category: a consumer requires the one it
|
||||||
|
can mount. If "give me the library, however" is ever needed, it is a provision an umbrella module
|
||||||
|
serves, not a seat.
|
||||||
|
|
||||||
|
Both are node-scoped, one holder per node (ADR 0110), so ace holds both. `nfs` and `samba` are the
|
||||||
|
first implementations; a second (Ganesha for `nfs-share`, ksmbd for `smb-share`) is what proves
|
||||||
|
0126's promise that "replacing the implementation changes nothing for any caller".
|
||||||
|
|
||||||
|
The holder module:
|
||||||
|
|
||||||
|
- declares the exported paths as `accesses` (ADR 0051: it owns nothing about them — never creates,
|
||||||
|
chowns or removes), and *which* paths as the assignment's settings (ADR 0046/0112);
|
||||||
|
- writes the share configuration (`/etc/exports`, `smb.conf`) as mesh-managed files and drives the
|
||||||
|
units, like `dnsmasq`/`sshd` do for theirs;
|
||||||
|
- declares its endpoints (`nfs` 2049/tcp; `smb` 445/tcp, …) so the reach — internal, or the LAN
|
||||||
|
once 154 has an answer — is the assignment's, and converge keeps them open;
|
||||||
|
- **provides** the seat's provision, so a consumer on another node `requires nfs-share` (or
|
||||||
|
`smb-share`) and reads `${bound:nfs-share:at}` and the path from its binding instead of a
|
||||||
|
hand-typed mount.
|
||||||
|
|
||||||
|
## The design gap this exposes
|
||||||
|
|
||||||
|
**A seat definition has no home outside the module that first declared it.** Today a seat is
|
||||||
|
declared inside a manifest (`showcase` declares `the-showcase`, `ca-trust` its own). If `nfs`
|
||||||
|
declared `nfs-share`, Ganesha could hold it only by depending on nfs's manifest — the coupling
|
||||||
|
0126 removed for callers, reintroduced for implementations. The protocol needs a neutral place in
|
||||||
|
the catalogue beside the modules (a seat definition registered like a manifest), with a module
|
||||||
|
saying which seats it implements. This is the first role with an obvious second implementation,
|
||||||
|
which is what makes it the exemplar for that mechanism.
|
||||||
|
|
||||||
|
## The consumer's half: a module mounts it (2026-09-30, third and fourth round)
|
||||||
|
|
||||||
|
A binding tells a consumer *where* the share is; it does not put the files on its machine. Mounting
|
||||||
|
is something done on a machine, and something done on a machine is a module's work — not the host's
|
||||||
|
(the vocabulary stays closed; no `mount` resource kind).
|
||||||
|
|
||||||
|
**A consumer-side module, `network-share` — the module responsible for setting up the network
|
||||||
|
shares a node uses** (the operator's framing). A node role, like `node-uplink` or
|
||||||
|
`node-dns-resolver`: each machine has it at most once, which is a reason for it to hold a
|
||||||
|
node-scoped seat, so two modules can never both be writing mount units on one machine. Assigned on
|
||||||
|
the node that wants the files:
|
||||||
|
|
||||||
|
- `requires nfs-share` (or `smb-share`); several shares on one node are several local names of
|
||||||
|
the requirement (ADR 0094);
|
||||||
|
- its manifest is a `package` (nfs-utils), a `file` writing a systemd `.mount` unit filled from
|
||||||
|
the binding — `What=${bound:nfs-share:at}:${bound:nfs-share:path}` — and a `service` enabling it
|
||||||
|
after the overlay is up: the same shape as `resolv-conf` or `sshd`, files and a unit;
|
||||||
|
- *where* it mounts is the assignment's setting (`/srv/media` on one machine, elsewhere on
|
||||||
|
another); which machine mounts what is an operator decision made at assignment, exactly as which
|
||||||
|
paths a machine shares is.
|
||||||
|
|
||||||
|
**The modules that use the files never learn about NFS.** A player, an indexer, a backup declares
|
||||||
|
the mounted path as an `access` — an operator-chosen, pre-existing path the mesh never owns
|
||||||
|
(ADR 0051), exactly as `/storage/media` is on ace. The same app manifest then runs on ace against
|
||||||
|
the local library and on another node against the mounted one, with only its assignment differing.
|
||||||
|
|
||||||
|
**The one check to add, because it is the data-loss case.** An `access` is confirmed today by the
|
||||||
|
path being present. For a mountpoint that is not enough: a writer whose container starts before the
|
||||||
|
mount is up writes into the empty directory underneath it, and the files vanish when the mount
|
||||||
|
lands. The access check must confirm the path is *a mountpoint* when the module says so (or the
|
||||||
|
module's unit is ordered before the consumer's container — which crosses modules and is exactly
|
||||||
|
what the mesh does not order). Which of the two is the decision's.
|
||||||
|
|
||||||
|
**Identity crosses the wire.** `sec=sys` NFS trusts the client's uid, so a consumer must run as the
|
||||||
|
library's owner on the server (ace: `media`, 1001:2000) — hq 153's `${access:<id>:uid}`, read from
|
||||||
|
the mounted tree, answers it on the consumer's side too.
|
||||||
|
|
||||||
|
## Open questions for the decision
|
||||||
|
|
||||||
|
- Whether an NFS export over the overlay is an `internal` reach of the same endpoint or a second
|
||||||
|
export line — NFS authorises by client address, so the mesh range and the LAN range are two
|
||||||
|
entries in one file.
|
||||||
|
- How a consumer's binding expresses a *path* to mount (today bindings carry `at`, `port`, `as` and
|
||||||
|
whatever the provider `serves`), and whether one share can serve several paths.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-09-30
|
||||||
|
located-in:
|
||||||
|
- mesh-controller internal/catalogue/resolve.go (holdings are derived from every resolved assignment's manifest `claims`)
|
||||||
|
- mesh-controller cmd/mesh-controller/seats.go (the deliberate act exists — HoldSeat, "recording … as its standing holder" — beside it)
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 170 — Assigning a module claims every seat it could hold
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
ace's migration needs a postgres of its own: the operator's decision is that a `postgres` module
|
||||||
|
assigned on ace provides `postgres-database` to ace's modules and has **nothing to do with the
|
||||||
|
`mesh-store` seat**, which novox's assignment holds by a deliberate act already taken ("make
|
||||||
|
novox's postgres the mesh-store").
|
||||||
|
|
||||||
|
`assign ace postgres` (2026-09-30):
|
||||||
|
|
||||||
|
```
|
||||||
|
ace is assigned postgres
|
||||||
|
|
||||||
|
AND 1 other machine(s) cannot be worked out as things stand, so nothing will be sent to them:
|
||||||
|
novox
|
||||||
|
- postgres on novox claims "mesh-store", which postgres on ace already holds — one per mesh
|
||||||
|
mesh-controller: these assignments cannot be applied:
|
||||||
|
- postgres on ace claims "mesh-store", which postgres on novox already holds — one per mesh
|
||||||
|
```
|
||||||
|
|
||||||
|
The second assignment did not merely fail: it made **the control plane's own store's
|
||||||
|
assignment unresolvable** until unassigned. Nothing was pushed; the state is restored.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
`resolve.go` derives what a node holds from the manifest's `claims` of every module resolved on
|
||||||
|
it, so a claim in a definition is a claim by every assignment of that module. The deliberate
|
||||||
|
act ADR 0110 describes exists beside it — `seat …` records "X on Y as its standing holder"
|
||||||
|
(`HoldSeat`) — but resolution does not consult that record; it consults the manifests.
|
||||||
|
|
||||||
|
## What was decided
|
||||||
|
|
||||||
|
[ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md):
|
||||||
|
|
||||||
|
> **A definition says which seats a module *can* hold. An assignment says which it *does*
|
||||||
|
> hold.** The store module can hold `mesh-store`, and it may be assigned to every node. Exactly
|
||||||
|
> one of those assignments holds the seat, because that assignment said so.
|
||||||
|
|
||||||
|
The manifest's `claims` is being read as *does hold*.
|
||||||
|
|
||||||
|
## What would be right
|
||||||
|
|
||||||
|
Resolution takes the holder of a seat from the recorded holding (the seat's standing holder),
|
||||||
|
not from the manifests: a module whose definition can hold a seat is assignable anywhere, and only
|
||||||
|
the assignment recorded as holder claims it — with the refusal reserved for a second *recorded*
|
||||||
|
holder at the seat's scope. Assigning postgres to ace is then exactly what the operator said it
|
||||||
|
is: a database provider on ace, and no more.
|
||||||
|
|
||||||
|
## Until then
|
||||||
|
|
||||||
|
`postgres` cannot be assigned on any second node; ace's database windows (baserow, letta, n8n,
|
||||||
|
car-hunter, txt-game) wait on this.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-09-30
|
||||||
|
located-in: [mesh-controller internal/catalogue/settings.go (settle), mesh-controller internal/catalogue/declaration.go (composed, ownNames)]
|
||||||
|
fixed-by: mesh-controller PR 175 (a setting overrides a declared key and adds none; `${setting:…}` in a served or contributed value); mesh-catalog PR 196 (mail declares its domain, the identity provider its issuer)
|
||||||
|
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 173 — A module's settings reach every fact it contributes, not only the file that asked
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
Setting the mail module's operator values — `domain`, `sitename`, `website`, `proxy-address` — so that
|
||||||
|
its environment file could read them as `${setting:…}`
|
||||||
|
([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)),
|
||||||
|
and then planning the control node, showed the four keys in places nothing asked for them:
|
||||||
|
|
||||||
|
- in every **route** the module contributes to the proxy, beside `label`, `endpoint` and `port`;
|
||||||
|
- in the **database** it contributes to the store's provider, beside the database's `name`;
|
||||||
|
- in the `smtp` facts every **consumer** of its mail provision is bound to.
|
||||||
|
|
||||||
|
Nothing broke: a provider ignores a key it does not read. But a proxy now receives a mail server's
|
||||||
|
`proxy-address` and `website` as if they were route facts, a consumer of mail is told the site's name,
|
||||||
|
and a reader of `plan` cannot tell which of a contribution's keys the module meant and which leaked in.
|
||||||
|
|
||||||
|
## Why this is here
|
||||||
|
|
||||||
|
Settings are one flat map per module, laid over every mergeable file, every contribution and every
|
||||||
|
served fact alike (`settle`). That was the right generality when a setting *was* a contribution's
|
||||||
|
override — a route's label is the example the code gives. It stops being right the day a setting is
|
||||||
|
an operator's value for one file, which ADR 0155 made ordinary. The design permits a value to travel
|
||||||
|
where nobody sent it, silently, and every consumer of a provision reads a map that grows with the
|
||||||
|
provider's unrelated settings.
|
||||||
|
|
||||||
|
[ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) and design 27
|
||||||
|
already say where this ends: a requirement has a contract, and a value goes to the requirement that
|
||||||
|
asked for it. Until that form exists, this is the cost of the placeholder being the first case of it.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- Should a key a file asks for with `${setting:<key>}` be withheld from contributions and served
|
||||||
|
facts, or should a contribution's overrides live under their own key (`contributes`, `serves`)?
|
||||||
|
The second is the shape design 27 draws; the first is the smaller change and keeps the leak from
|
||||||
|
widening while it is drawn.
|
||||||
|
- What does a consumer do with a served key it did not expect? Today: nothing, silently. A served
|
||||||
|
map is not checked against what the provision's contract says it carries, because there is no such
|
||||||
|
contract yet.
|
||||||
|
|
||||||
|
## Resolved, 2026-09-30
|
||||||
|
|
||||||
|
**A setting overrides a key a contribution or a served fact declares, and adds none.** A file keeps
|
||||||
|
taking any key, because a configuration file is where an operator adds things; a contribution and a
|
||||||
|
served fact are a contract the other side reads, and a setting made for one of the module's files is
|
||||||
|
no part of it. A key that lands nowhere — no mergeable file, no `${setting:…}` asking for it, no
|
||||||
|
contribution or served fact declaring it — is named as stray when the node is planned, rather than
|
||||||
|
dropped.
|
||||||
|
|
||||||
|
The first open question is answered the smaller way, and it turned out to be the right one: the two
|
||||||
|
keys consumers actually read through the leak — the mail provider's `domain`, the identity provider's
|
||||||
|
`issuer` — are now **declared** by the provider in what it serves, as the operator's value
|
||||||
|
(`${setting:domain}`, `${setting:issuer}`), filled from the same setting that used to leak and refused
|
||||||
|
by name when nothing sets it. So the contract says what travels, which is the shape design 27 draws,
|
||||||
|
without a second key for overrides. The second question stands: a consumer still checks nothing
|
||||||
|
against a contract, because there is none yet; what it is told is now only what the provider
|
||||||
|
declared.
|
||||||
|
|
||||||
|
*How it was checked:* the plans of all four nodes, under the running controller and the one with the
|
||||||
|
rule, compared resource by resource — every key that disappears from a contribution is a leaked file
|
||||||
|
setting or one of the mesh's own words (`expose`, `endpoints`), and nothing a provider reads goes
|
||||||
|
away; the two served keys were declared before the controller rolled. Unit tests: a setting a route
|
||||||
|
never declared does not reach the proxy; a served value nothing sets is refused by name; the setting
|
||||||
|
a served fact asks for is not stray.
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-09-30
|
||||||
|
located-in: [mesh-controller internal/catalogue/dir_into.go, mesh-controller internal/catalogue/declaration.go, mesh-catalog modules]
|
||||||
|
fixed-by: mesh-controller PR 175 (`place: "mesh"`, a directory beneath a placed one, the proof test resolving both sides); mesh-catalog PR 197 (48 definitions converted)
|
||||||
|
amended-design: [03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md, 03-DESIGN/01-to-be/18-building-a-module.md]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 174 — The mesh's own files for a module are placed by the definition, not by the mesh
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
After every module's *own* data directory was placed by the mesh
|
||||||
|
([issue 119](../119-a-module-definition-decides-where-its-files-live/00-report.md)), the catalogue
|
||||||
|
still carries **232 host paths in 50 definitions**, all of one kind: where the mesh writes what it
|
||||||
|
makes *for* the module — its sealed bus credential (`own-secrets.broker`), its merged config file, its
|
||||||
|
bindings — under `/var/lib/mesh/<module>/…`, and the directory resource that creates that subtree.
|
||||||
|
Not one of those files is the module's. The mesh mints the credential, composes the binding, merges
|
||||||
|
the config; the definition only says where to put them, and says it the same way seventy times.
|
||||||
|
|
||||||
|
## Why this is here
|
||||||
|
|
||||||
|
Design 27's answer to *what sits beneath a node's root* (2026-09-26) is that the mesh's writes need no
|
||||||
|
module-visible reservation: **what the mesh writes for a module is the mesh's plumbing, placed where
|
||||||
|
the mesh chooses and mounted in, never part of the module's contract.** The definition today names
|
||||||
|
that place, so a definition is not yet free of host paths — and a node whose root is elsewhere would
|
||||||
|
place the module's data there and the mesh's files still under `/var/lib/mesh`.
|
||||||
|
|
||||||
|
The path-preserving test that let issue 119 close does not cover this: it proves a *placed* directory
|
||||||
|
resolves to what was named, and these are not placed.
|
||||||
|
|
||||||
|
## What it would take
|
||||||
|
|
||||||
|
A word for "the mesh's file for this module", or none: `own-secrets` values, a merged config file and
|
||||||
|
a binding could be named by key alone, with the mesh choosing `<root>/mesh/<module>/<key>` and
|
||||||
|
mounting it where the container says. The container side of the mount already exists in every
|
||||||
|
definition (`/run/secrets/broker`, `/run/config/config.json`); only the host side would go. The
|
||||||
|
change is in the controller, once, and then a mechanical edit of fifty definitions, which the same
|
||||||
|
test that proved 119 can prove again with the rule extended.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- Does `own-secrets` keep its map shape with the value becoming the *container* path rather than the
|
||||||
|
host path, or does the mount stay where it is and the host side become a placeholder the mesh
|
||||||
|
fills, `${mesh:<key>}`?
|
||||||
|
- A binding file today lands wherever `binds` says; a module's code reads it from an environment
|
||||||
|
variable naming the container path. If the host side is the mesh's, is the container side still the
|
||||||
|
definition's to choose? It should be: it is the software's contract.
|
||||||
|
|
||||||
|
## Resolved, 2026-09-30
|
||||||
|
|
||||||
|
The word is the one issue 119 introduced, with a second place: a directory saying `place: "mesh"` is
|
||||||
|
the mesh's directory for the module, `<root>/mesh/<module>`, beside the assignment's own root and
|
||||||
|
under the same node setting. The mesh's files keep their map shape and the container side of every
|
||||||
|
mount stays the definition's — only the host side changed, to `${dir:mesh-state}/…`. A directory that
|
||||||
|
sat beneath the mesh's (a forge's runtime state, a manager's output) states its path as
|
||||||
|
`${dir:mesh-state}/<rest>` and moves with it; one that sits elsewhere by adoption (the registry's data)
|
||||||
|
keeps its literal path as the exception it is.
|
||||||
|
|
||||||
|
Forty-eight catalogue definitions and the controller's own manifest were converted mechanically. The
|
||||||
|
proof is the same test that let issue 119 close, now resolving *both* checkouts before comparing,
|
||||||
|
because the earlier manifest already placed its own directories: resolved on the default root, every
|
||||||
|
converted definition names exactly the paths it named before. Nothing moved.
|
||||||
|
|
||||||
|
Both open questions are answered by keeping what exists: the map stays, the host side is placed; the
|
||||||
|
container side is the software's contract and stays where the definition says.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-09-30
|
||||||
|
located-in: [mesh-controller internal/broker/streams.go (the controller's EVENTS consumer), mesh-controller internal/link/receive_nats.go]
|
||||||
|
fixed-by: mesh-controller PR 173 (MaxAckPending 1 on the controller's events consumer); the packaging module's rebuild record and the status count remain open in the text below
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 175 — An announcement queued behind a long build comes back, and the build runs again
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
On the evening of 2026-09-30 five merges landed within minutes. The controller's log then showed the
|
||||||
|
same four announcements — two into this repository, one into the catalogue, one into the controller's
|
||||||
|
own — arriving again every couple of minutes, and each arrival of the controller's rebuilt the two
|
||||||
|
modules that package its source. The builder built `builder` and `route-proxy` five times over for one
|
||||||
|
merge, the control node was pushed after each, and every other message the controller handles waited
|
||||||
|
behind the builds. It looked like a slow mesh; it was a loop.
|
||||||
|
|
||||||
|
Earlier the same evening, at a lower rate, the log already carried duplicated lines — a merge seen
|
||||||
|
twice, a module "moved" twice — that nobody read as a symptom.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The controller acts on what it consumes in **one loop, one message at a time**, and a merge's handler
|
||||||
|
builds every module the merge changed before it returns — minutes of work. The bus's acknowledgement
|
||||||
|
window is thirty seconds. That contradiction was met once already: the message being worked on is kept
|
||||||
|
alive by a heartbeat while its handler runs
|
||||||
|
([issue 127](../127-a-module-event-derives-a-subject-nothing-publishes/00-report.md)'s stretch,
|
||||||
|
controller PR 122). **The heartbeat covers one message.** The consumer is a push consumer with no
|
||||||
|
bound on what it may have outstanding, so the client is handed everything that is waiting at once; the
|
||||||
|
messages queued behind the one being built time out unacknowledged, come back after thirty seconds,
|
||||||
|
and are handled again when the loop gets to them — including the merge whose builds are already done,
|
||||||
|
which builds them again. A module that only *packages* another repository's source has no record of
|
||||||
|
which commit it was last rebuilt for, so nothing says "already done".
|
||||||
|
|
||||||
|
## Why it matters beyond this instance
|
||||||
|
|
||||||
|
The design permits work to be done twice, silently, and the doubling scales with how busy the mesh
|
||||||
|
is: the busier the builder, the longer the queue, the more that comes back. A push to a machine is
|
||||||
|
idempotent and a rebuild produces the same digest, so nothing broke — but every merge cost several
|
||||||
|
builds, the control node was pushed after each, and a person watching saw a mesh that would not
|
||||||
|
settle. It was the redelivery storm of 2026-09-28 in a narrower form, one layer out.
|
||||||
|
|
||||||
|
## What would have prevented it
|
||||||
|
|
||||||
|
- **A consumer that is handled one at a time is delivered one at a time.** `MaxAckPending: 1` on the
|
||||||
|
controller's events consumer: the server holds the rest, nothing times out behind a build, and the
|
||||||
|
heartbeat that keeps one message alive is then keeping *the* message alive.
|
||||||
|
- **A packaging module records the commit it was last rebuilt for**, so a replayed announcement is
|
||||||
|
"already built from it", the answer the source-built modules already give.
|
||||||
|
- **A log line that appears twice with the same commit is a symptom**, and the check is cheap: the
|
||||||
|
same announcement acted on twice within its window is a count worth exposing in `status`.
|
||||||
|
|
||||||
|
## Resolved on the first remedy, 2026-09-30
|
||||||
|
|
||||||
|
`MaxAckPending: 1` on the controller's events consumer (mesh-controller PR 173): the server hands the
|
||||||
|
controller one announcement at a time and holds the rest, so nothing times out behind a build. The
|
||||||
|
existing consumer is brought to that configuration by the assertion the controller makes at start.
|
||||||
|
The second and third remedies — a packaging module recording the commit it was last rebuilt for, and
|
||||||
|
a doubled announcement counted in `status` — are not built; they would make the same fault visible
|
||||||
|
and cheaper should the first ever be undone, and they are left here as what to reach for then.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-10-01
|
||||||
|
located-in: [mesh-controller cmd/mesh-controller/seatverbs.go (argvFor, "build": `--wait 0`, no `--self`), mesh-controller cmd/mesh-controller/main.go (builds.Built records a build and registers nothing)]
|
||||||
|
fixed-by: mesh-controller PR 179 (one take-in for a build's outcome, called by the waiting command and by the daemon; `--wait 0` asks and returns the id; the seat verb says `--self` for a forge path); ADR 0157 (mesh-controller PR 178) gave the tool something to hear
|
||||||
|
amended-design: 03-DESIGN/01-to-be/18-building-a-module.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 176 — The console's `build` tool neither waits nor registers, and does not take a forge path
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
Eight media modules held by the mesh had not been rebuilt after their manifests changed, because a
|
||||||
|
merge rebuilds only the modules whose recorded source is the merged repository and theirs was another
|
||||||
|
one. Asked through the console — the controller's `build` tool, served on its seat
|
||||||
|
([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)) —
|
||||||
|
with the repository given as its path on the forge, the way the tool's own description invites:
|
||||||
|
|
||||||
|
- every call answered at once with *no build machine answered within 0s … the work is queued*;
|
||||||
|
- the builder, asked in the same breath, failed each one with *cannot clone novox/mesh-catalog*: the
|
||||||
|
path was handed to `git clone` as written, because the tool never says the repository is a path on
|
||||||
|
the forge holding the git seat (`--self`), which the command line requires for that form;
|
||||||
|
- given the repository's URL instead, the call still answered within zero seconds, the build ran on
|
||||||
|
the builder, its result was heard and recorded — and the module was **not registered**: what hears a
|
||||||
|
finished build records the build and stops; only the caller that waited would have parsed the
|
||||||
|
manifest and registered it, and the caller had gone.
|
||||||
|
|
||||||
|
So the console can start a build and never learn its outcome, and a build it starts cannot change
|
||||||
|
what the mesh holds. The tool's description — *have the build machine build a repository and record
|
||||||
|
what came out* — is true of the build record and false of the module.
|
||||||
|
|
||||||
|
## Why this is here
|
||||||
|
|
||||||
|
The seat verb was written as fire-and-forget, deliberately (`--wait 0`), so that a tool call over the
|
||||||
|
bus does not sit for the minutes a build takes. That reasoning moved the wait but not the work that
|
||||||
|
followed it: registration lives in the waiting caller, not in the path that hears the result. The two
|
||||||
|
halves of "build" — asking, and taking in what came back — are split across the command and the
|
||||||
|
event handler, and the tool reaches only the first.
|
||||||
|
|
||||||
|
The forge-path form is a second, smaller gap: the seat verb maps three arguments and forgets the flag
|
||||||
|
the same command needs to read one of them.
|
||||||
|
|
||||||
|
## What would be right
|
||||||
|
|
||||||
|
Registration belongs where the result is heard, once, so a build's outcome reaches the mesh whoever
|
||||||
|
asked and whether or not they waited — the same rule as an announcement's builds. The tool then
|
||||||
|
answers with what it can say at once (asked, queued, or refused) and `builds` says the rest. A
|
||||||
|
repository given without a scheme is a path on the git seat, and the verb says so.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- Should a tool call be able to wait at all? A builder answers in minutes; the console's transport
|
||||||
|
holds a call for a bounded time. If not, the tool needs a way to follow one build — which is the
|
||||||
|
builder's missing progress (no tools, no events) named in the console's review of 2026-10-01.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01
|
||||||
|
|
||||||
|
Registration moved to where the outcome is heard. One function takes a build's outcome in — records
|
||||||
|
the build, parses the manifest, refuses a definition that names an installation, registers the module
|
||||||
|
with its source as the seat and path the request carried and the outcome echoes — and both the
|
||||||
|
command that waited and the daemon that follows the role's `built` event call it. So a build asked
|
||||||
|
for by anything that could not wait reaches the catalogue the same as one asked for by hand, and the
|
||||||
|
same outcome heard twice writes one row twice with the same values.
|
||||||
|
|
||||||
|
The tool keeps not waiting, and says so: `build --wait 0` publishes the work and answers with the
|
||||||
|
build's id, and `builds --log <id>` follows the build line by line ([ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)),
|
||||||
|
which is what a tool call over the bus can do in the seconds it has. A repository given without a
|
||||||
|
scheme is said to be a path on the git seat, so the forge-path form the tool's description invites
|
||||||
|
now works.
|
||||||
|
|
||||||
|
The open question is answered by the shape: a tool call does not wait; it asks, gets the id, and
|
||||||
|
follows. *How it is checked:* the shared take-in against a raised store — registered with the seat
|
||||||
|
source, a definition naming an installation recorded and refused, a failure said in the builder's
|
||||||
|
words — and the tool's mapping of a forge path against a URL.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-10-01
|
||||||
|
located-in: [mesh-controller cmd/mesh-controller/sendable_test.go (the converged-declaration guard), mesh-controller cmd/mesh-controller/adopting_test.go (the adopted-anchor fixture), the build of the controller (runs no check)]
|
||||||
|
fixed-by: mesh-controller PR 180 (the two tests, for what they missed); the process half is open below
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 177 — The controller's check is run by nobody, and two of its tests failed for days unseen
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
`make check` on the controller's main failed two store-backed tests on 2026-10-01, both for
|
||||||
|
reasons older than that day:
|
||||||
|
|
||||||
|
- the guard that holds a converged declaration byte for byte to what an older host was sent still
|
||||||
|
expected a `hosts` list on every container, after the change of 2026-09-30 that took it off — a
|
||||||
|
machine's own resolver knows the mesh's names now ([issue 171](../171-a-modules-own-resolver-knows-no-mesh-name/00-report.md));
|
||||||
|
- the adopted machine in the converge test reported no outward link, after
|
||||||
|
[ADR 0140](../../02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md) (2026-09-28)
|
||||||
|
made a filter depend on one.
|
||||||
|
|
||||||
|
Neither commit touched the test it broke, and neither merge failed: the mesh builds the controller
|
||||||
|
from its repository and runs none of its tests. The tests that need a store — the ones that say what
|
||||||
|
a machine is actually sent — are exactly the ones a quick `go test ./...` skips, so a person running
|
||||||
|
the fast check sees green too. Every merge tonight, this one included, was checked that way.
|
||||||
|
|
||||||
|
## Why this is here
|
||||||
|
|
||||||
|
A guard that is not run is a comment. The byte-for-byte guard exists because an older host parses a
|
||||||
|
declaration strictly and a field it does not know is a machine that applies nothing
|
||||||
|
([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)); it went
|
||||||
|
red on a change that happened to be safe — a field removed — and would have gone red the same way on
|
||||||
|
one that was not. The mesh has a rule that a test defends a decision
|
||||||
|
([ADR 0017](../../02-DECISIONS/0017-a-test-defends-a-decision.md)) and no rule that says when the
|
||||||
|
test is run.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01 — the tests
|
||||||
|
|
||||||
|
The guard is re-captured with the change named in its own comment: a field an older host never sees
|
||||||
|
is the one change the guard permits, a field it would refuse is the one it exists to catch. The
|
||||||
|
fixture reports an outward link as a real host does. `make check` is fully green on main again
|
||||||
|
(mesh-controller PR 180). No code changed.
|
||||||
|
|
||||||
|
## Open — the process
|
||||||
|
|
||||||
|
The build should run the check, or something should, before a merge lands. What that is — the
|
||||||
|
builder raising the store the tests need, a check the forge runs on a pull request, or the controller
|
||||||
|
refusing to record a build whose repository's own check fails — is a decision not taken here. Until
|
||||||
|
it is, `make check` before a controller merge is the operator's habit, written into the work order,
|
||||||
|
and this issue stays the record of why.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-10-01
|
||||||
|
located-in: [mesh-controller cmd/mesh-controller/plan.go (routeNamesInTheMesh), mesh-controller internal/catalogue (NamesServed)]
|
||||||
|
fixed-by: mesh-controller PR 181 (every node's resolution read first, names attributed across them to the terminus, in order; the many shape of contributions counted)
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 178 — A routed name resolves to a provider that was merely told it, and flips between plans
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
From the home server, the dashboard's public name resolved inside the mesh to the control node, where
|
||||||
|
no proxy serves it, and TLS failed; from outside it resolved to the home server and worked. Filed
|
||||||
|
first in the forge's tracker on the hq repository (its issue 227), on 2026-09-30. The same night,
|
||||||
|
two plans of the same machine taken a minute apart differed in exactly one resource — the mesh's
|
||||||
|
names region — with the dashboard's name on one node's address and then on the other's.
|
||||||
|
|
||||||
|
A second thing hid behind it: no module that is routed under several names — the photo service's
|
||||||
|
six, the invoicing service's two, the mail server's five — had any of them in the names region at
|
||||||
|
all.
|
||||||
|
|
||||||
|
## Why this is here
|
||||||
|
|
||||||
|
The names region is composed from every contribution the mesh gave a name to. A consumer that is
|
||||||
|
routed contributes its label to its route, and the proxy serves the composed name. A consumer that
|
||||||
|
also uses an identity provider contributes the same label there, because the provider must know the
|
||||||
|
consumer's public name to compose a redirect — and by the rule that two readers must agree
|
||||||
|
([issue 122](../122-a-module-cannot-ask-for-its-own-public-name/00-report.md)) it is
|
||||||
|
given the same composed name. So one name reaches two providers, and the code attributed it to the
|
||||||
|
node of whichever contribution a map yielded last. Map order is not stable between runs; the region
|
||||||
|
was not either.
|
||||||
|
|
||||||
|
The second fault is older: the single-value reading of a module's contributions is deliberately
|
||||||
|
empty when the module contributes several times to one requirement
|
||||||
|
([ADR 0094](../../02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md)'s
|
||||||
|
sibling), and the names region used that reading, so a module with several routes named none.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01
|
||||||
|
|
||||||
|
Every node's resolution is read first, and the names are attributed across them at once. **The
|
||||||
|
terminus serves the name**: among the providers a name reaches, the one that is not itself published
|
||||||
|
under a labelled name through another provider. An identity provider is routed through the proxy and
|
||||||
|
so is a consumer of names, not their end; the proxy contributes no label to anyone and is. The rule
|
||||||
|
knows nothing of what "route" means — it reads the graph the modules declared — and it walks nodes,
|
||||||
|
requirements and contributions in order, so one mesh yields one region. Every shape of contribution
|
||||||
|
is counted, so a module routed under several names has every one of them resolved.
|
||||||
|
|
||||||
|
*How it is checked:* the two-node mesh of the report, resolved and attributed twenty-five times — the
|
||||||
|
dashboard's name on the home server, the identity provider's own name on the control node, no name
|
||||||
|
leaked to the identity provider; a module with two routes yields two names; and, live, two plans of
|
||||||
|
the home server after the roll-out identical in the names region, with the dashboard's name at the
|
||||||
|
home server's address and the several-routed modules' names present.
|
||||||
|
|
||||||
|
## Note on where this was filed
|
||||||
|
|
||||||
|
The symptom was filed in the forge's issue tracker, which is not where hq's issues live: a tracker
|
||||||
|
issue carries no status the mesh's records read, and its numbers collide with hq's pull request
|
||||||
|
numbers in conversation. It is closed there pointing here.
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-10-01
|
||||||
|
located-in: [the identity provider's assignment on the control node (an adopted database whose admin predates the mesh), mesh-catalog modules/keycloak (the minted `admin` own-secret, applied by the server only when it creates its master realm)]
|
||||||
|
fixed-by: done by hand on 2026-10-01 through the server's own bootstrap command — the admin's password set to the value the mesh minted; no code changed
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 179 — An adopted identity provider's admin never took the secret the mesh minted
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
Filed first in the forge's tracker on this repository (its issue 228, 2026-09-30). The identity
|
||||||
|
provider's sidecar failed every call with *401 invalid_grant, Invalid user credentials*, and the
|
||||||
|
server logged a login error for `admin-cli` every five seconds. The provisioner that creates a client
|
||||||
|
for every consumer of `oidc-client` could never create one, so the dashboard's and the car service's
|
||||||
|
single sign-on on the home server failed at the identity provider. Not new that day: present since
|
||||||
|
the module moved to the mesh.
|
||||||
|
|
||||||
|
## Why this is here
|
||||||
|
|
||||||
|
The manifest mints an `admin` own-secret and renders it into the server's environment and the
|
||||||
|
sidecar's file. The server reads that variable only when it creates its master realm. This instance
|
||||||
|
was adopted with its database, whose `admin` user dates from 2022; the real password predates the
|
||||||
|
mesh, and the minted one was inert from the first start. An own-secret the mesh mints is a statement
|
||||||
|
the module's software is expected to honour, and adopted software that already holds its own
|
||||||
|
credential does not — the same shape as the download client's web password on the home server
|
||||||
|
([ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md)'s
|
||||||
|
operator-delivered secret), met from the other side.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01
|
||||||
|
|
||||||
|
Reality was made to match the mesh rather than the mesh told about reality: the server's own
|
||||||
|
bootstrap command created a temporary admin, that admin set `admin`'s password to the value the
|
||||||
|
mesh minted — read from the sidecar's mounted secret on the machine, never printed — and the
|
||||||
|
temporary admin was removed. Within a minute the sidecar listed realms through the console, and the
|
||||||
|
two consumers' clients, `mesh_ace_grafana` and `mesh_ace_carhunt`, existed in the realm.
|
||||||
|
|
||||||
|
The alternative, `secret accept` with the real password, was not available: the predecessor's
|
||||||
|
configuration directory is gone and the password with it. For the next adopted module that holds a
|
||||||
|
credential the mesh mints, the choice is the same, and the mesh's word for the second path is still
|
||||||
|
only the download client's: a credential the mesh cannot make is the operator's to deliver.
|
||||||
|
|
||||||
|
*How it was checked:* `keycloak_list_realms` through the console answers; `keycloak_list_clients`
|
||||||
|
on the realm lists both mesh-named clients; the server's log stops the five-second login error.
|
||||||
|
|
||||||
|
## Open
|
||||||
|
|
||||||
|
The manifest still says the admin password is the mesh's to mint, which is true of a fresh install
|
||||||
|
and false of an adopted one, and nothing in a definition can say which it will be. Whether an
|
||||||
|
own-secret should be acceptable the way a pair secret is — `secret accept <node> <module> <name>`
|
||||||
|
already exists and takes an own-secret — is a sentence for design 27's operator provider, not taken
|
||||||
|
here.
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-10-01
|
||||||
|
located-in: [mesh-controller cmd/mesh-controller/secret.go (accept and now rotate), mesh-controller internal/inventory/secrets.go (a module's own secret), mesh-controller internal/catalogue/manifest.go (how an own secret is taken), the controller's seat (rotate was not a verb)]
|
||||||
|
fixed-by: mesh-controller PR 183 (`secret rotate`, the `taken` word on an own secret, `rotate` on the controller's seat with two shapes); the staged form for an applied secret stays open below
|
||||||
|
amended-design: [03-DESIGN/01-to-be/13-credentials-and-their-rotation.md, 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 180 — A module's own secret cannot be rotated, and nothing rotates from the console
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
Filed first in the forge's tracker on this repository (its issue 231, 2026-09-30), after a module's
|
||||||
|
API token was printed by accident on the home server and the only way to change it was to generate
|
||||||
|
a value by hand and `secret accept` it. The report said there was no rotation at all. That was half
|
||||||
|
right: `rotate <provision>` has existed for a pair credential since design 13, pushing both ends
|
||||||
|
together; what did not exist was any rotation of a **module's own secret** — a token, an application
|
||||||
|
secret, an administrator — and any way to ask for either through the console
|
||||||
|
([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
|
||||||
|
|
||||||
|
## Why this is here
|
||||||
|
|
||||||
|
[ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md) decided how a
|
||||||
|
single party's credential rotates: in place, in one of two forms. **Read at start** — the vault
|
||||||
|
delivers the new value as current and the party is started again. **Applied** — the party's own
|
||||||
|
code applies the value to a backend that takes it once, so the new value must be staged beside the
|
||||||
|
current one until the party confirms it. Neither form was built, and nothing said which form a given
|
||||||
|
secret needed. That last gap is the dangerous one: [issue 179](../179-an-adopted-identity-providers-admin-never-took-the-minted-secret/00-report.md)
|
||||||
|
is what a value looks like when the mesh believes it was taken and the software never read it. A
|
||||||
|
rotation that made that happen on purpose would be worse than no rotation.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01 — the read-at-start form, and the verb
|
||||||
|
|
||||||
|
**An own secret says how it is taken.** In a definition, `"own-secrets": {"api-token": {"path": …,
|
||||||
|
"taken": "at-start"}}` says the module reads the file when it starts; `"taken": "applied"` says its
|
||||||
|
own code applies the value to a backend; a path alone says neither. A definition that says neither
|
||||||
|
is not rotated by the mesh, and the refusal names the word to write.
|
||||||
|
|
||||||
|
**`secret rotate <node> <module> <name>`** makes the secret anew the way the first mint did, seals it
|
||||||
|
to the machine and to the operator, and sends the machine, so the module starts again on the new
|
||||||
|
value, under the same `restart-on` that any changed file triggers. Said in the log with who asked and
|
||||||
|
when, never the value. An applied secret is refused by name, until the staged form exists. A value
|
||||||
|
given to the mesh rather than made by it is refused as [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)
|
||||||
|
says, with the way out: change it where it lives, then accept the new value.
|
||||||
|
|
||||||
|
**`rotate` is a verb on the controller's seat** ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)),
|
||||||
|
with the two shapes the mesh has: a pair credential by provision and consuming machine, or an own
|
||||||
|
secret by machine, module and name. The console can ask for either.
|
||||||
|
|
||||||
|
Nothing in the catalogue says `taken` yet: the word ships one release ahead of its first use, and the
|
||||||
|
first definitions to say it follow once this controller runs.
|
||||||
|
|
||||||
|
*How it is checked:* the manifest form and its refusals; the rotation against a raised store —
|
||||||
|
rotates a secret taken at start, refuses an applied one, an undeclared one, an accepted one and an
|
||||||
|
unknown name, each in its own words; the verb's two shapes; and, live, a secret rotated through the
|
||||||
|
console on a module that reads it at start, the module restarted, and the module working.
|
||||||
|
|
||||||
|
*Done live, 2026-10-01 10:13 UTC:* the search module on the home server, the first definition to say
|
||||||
|
`taken: at-start` whose value the mesh had made. Asked through the console; the controller said it was
|
||||||
|
rotated and sent the machine; twenty-seven seconds later the secret file carried a new write time, the
|
||||||
|
server container had restarted on it, and the module answered. The two secrets filed in the forge's
|
||||||
|
report, the automation module's token and admin password, were refused: both had been accepted by hand
|
||||||
|
during adoption, and that refusal is the one ADR 0113 asks for — something outside the mesh may hold
|
||||||
|
an accepted value, so replacing it is a person's act. Modules whose source is pinned to a commit are
|
||||||
|
not rebuilt by a merge; their new definition was registered by asking for a build of main through the
|
||||||
|
console, which since [issue 176](../176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md)
|
||||||
|
registers what it hears.
|
||||||
|
|
||||||
|
## Open — the applied form
|
||||||
|
|
||||||
|
The staged rotation ADR 0114 decided for an applied secret is not built: the vault delivering the
|
||||||
|
new value beside the current one, the module's own code switching the backend and confirming, and
|
||||||
|
only then the new value current. It needs a word in the module's protocol for *confirm*, and it is
|
||||||
|
the form the identity provider's administrator and every database's superuser need. The request's
|
||||||
|
second half — re-issuing a pair credential through the provider's own code rather than by re-minting
|
||||||
|
and pushing — is the same shape from the provider's side, and sits with it.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-09-30
|
||||||
|
located-in:
|
||||||
|
- mesh-controller cmd/mesh-controller/modules.go (assign takes no provider; pin is a separate, per-machine command)
|
||||||
|
- mesh-controller internal/inventory (provision_pin keyed by (node, name))
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 181 — An assignment does not record which provider answers it
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
Planning ace's modules that need a database (baserow, letta, n8n, and the apps using ace's
|
||||||
|
predecessor postgres). The operator's model — and ADR 0110's — is that **an assignment states where
|
||||||
|
each of its requirements is answered from**: gitea's assignment on novox says its `postgres-database`
|
||||||
|
comes from novox; an app assigned to ace says whether its database comes from ace or from novox.
|
||||||
|
|
||||||
|
The mesh holds no such statement for any assignment. Read on novox (2026-09-30):
|
||||||
|
|
||||||
|
```
|
||||||
|
select … from provision_pin; -- 0 rows
|
||||||
|
```
|
||||||
|
|
||||||
|
Every requirement in the mesh resolves implicitly, each time, by ADR 0084's order (a pin, then the
|
||||||
|
provider on the consumer's own node, then the only provider).
|
||||||
|
|
||||||
|
## What was decided, and what exists
|
||||||
|
|
||||||
|
[ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md):
|
||||||
|
|
||||||
|
> Where several remain and none is local, **a person chooses when the module is assigned**.
|
||||||
|
> Assignment lists the candidates, with the holder of a seat that delivers the provision suggested
|
||||||
|
> first, and records the answer on the assignment as its pin. Without an answer the module is not
|
||||||
|
> assigned.
|
||||||
|
|
||||||
|
What the control plane implements:
|
||||||
|
|
||||||
|
| decided | implemented |
|
||||||
|
|---|---|
|
||||||
|
| the answer is recorded **on the assignment** | `provision_pin` is keyed `(node, name)` — one answer per machine per provision, shared by every module on it |
|
||||||
|
| chosen **at assignment** | `assign <node> <module>` takes no provider; `pin <node> <provision> <from-node>` is a separate command |
|
||||||
|
| an assignment may be answered from its own machine (gitea ← novox) | `pin` refuses a machine pinning to itself ("does not need saying") |
|
||||||
|
| every assignment has an answer | none recorded; resolution guesses the same answer every time |
|
||||||
|
|
||||||
|
## Consequence
|
||||||
|
|
||||||
|
Nothing is wrong *today* — with one postgres provider, every guess is the intended answer. But the
|
||||||
|
answer is not a fact anyone stated, so:
|
||||||
|
|
||||||
|
- **it changes silently** the day a second provider appears (e.g. a postgres assigned on ace): every
|
||||||
|
unpinned consumer re-resolves — a consumer on ace moves from novox's database to an empty one on ace
|
||||||
|
at the next push, which is data a module stops seeing without anything saying so;
|
||||||
|
- two modules on one machine cannot take one provision from different providers;
|
||||||
|
- a person reading an assignment cannot see where its data lives.
|
||||||
|
|
||||||
|
## What would be right
|
||||||
|
|
||||||
|
ADR 0110 as written: `assign` records, per requirement, the node that answers it (its own node
|
||||||
|
included), offering the candidates and refusing an assignment without an answer where several exist;
|
||||||
|
the per-machine `provision_pin` becomes a per-assignment record, with existing assignments backfilled
|
||||||
|
from what they resolve to now so nothing moves.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-10-01
|
||||||
|
located-in: [mesh-tools src/broker-nats.ts (one subject, one queue group per module), mesh-tools src/mcp.ts (no way to name a machine), mesh-tools src/runtime.ts (a claimed seat's verbs served by nobody), mesh-controller internal/broker/nats.go (the grant for a tool named one subject)]
|
||||||
|
fixed-by: mesh-tools PR (feat/a-tool-call-names-the-machine) and mesh-controller PR (same branch) — see ADR 0159; the store seat's verbs and postgres's tools follow in the catalogue
|
||||||
|
amended-design: [03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md, 03-DESIGN/01-to-be/34-the-console.md]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 182 — A tool call reaches whichever instance answers first, and a claimed seat's verbs are served by nobody
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
Asked how to list the databases of the store on one machine, the mesh had no answer. A module's tools
|
||||||
|
are served on one subject per module, `mesh.mod.<module>.tool.<name>`, and every instance of the
|
||||||
|
module joins one queue group on it, so a call to the database engine's tool while it runs on two
|
||||||
|
machines reaches whichever answered first, and the answer does not say which. There is no way to ask
|
||||||
|
the instance on one machine. The console lists the tool once and offers no machine.
|
||||||
|
|
||||||
|
And the seat half was missing too. Design 33 §3 says holding a seat means serving its tools, and
|
||||||
|
ADR 0154 built that for the controller's own seat alone. A module that holds a seat — the database
|
||||||
|
engine on the control node holding `mesh-store` — served none of the seat's verbs, because no seat
|
||||||
|
but the controller's declares any and no runtime knew which seats its module claimed.
|
||||||
|
|
||||||
|
## Why this is here
|
||||||
|
|
||||||
|
Both are the same omission: the tool surface was built as if every module ran on one machine and
|
||||||
|
held no seat. A queue group is the right default for a stateless module answering anywhere, and the
|
||||||
|
wrong only choice for a module whose instances are different things — two stores with different
|
||||||
|
databases. The architecture had the distinction: a module is a thing that runs on machines, a seat is
|
||||||
|
a role one of them holds. The tool surface did not carry it.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01
|
||||||
|
|
||||||
|
[ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md).
|
||||||
|
Every instance serves its module's subject twice: in the queue group as before, and with its own
|
||||||
|
machine as the subject's last token. `<module>.<tool>@<node>` reaches one machine's instance; the
|
||||||
|
console lists `node` on every module tool and puts it in the subject, never in the module's
|
||||||
|
arguments; every answer carries the machine that gave it, and the console appends it as its own line.
|
||||||
|
The grant for a tool covers both subjects.
|
||||||
|
|
||||||
|
A holder's runtime serves its seat's verbs: the credential the mesh writes names the seats the module
|
||||||
|
claims and the verbs each promises, the runtime serves each verb with the module's tool of the same
|
||||||
|
name on the seat's own subject, flat for a mesh seat and with the machine for a node-scoped one, and
|
||||||
|
the bus admits the subscription only where the module holds the seat. The store seat's first verbs
|
||||||
|
and the database engine's tools for them are the catalogue's next step, recorded in the decision.
|
||||||
|
|
||||||
|
*How it is checked:* against a real bus, a module on two machines answers each by name and says who
|
||||||
|
answered when unnamed, and a claimant answers a seat's verb on the seat's subject; the console lists
|
||||||
|
`node` on a module's tool and not on a seat's; the grant for a tool covers both subjects; and, live,
|
||||||
|
the store's databases listed from one named machine through the console.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-10-01
|
||||||
|
located-in: [mesh-controller internal/broker/nats.go (the controller's own publish grant)]
|
||||||
|
fixed-by: mesh-controller PR 189 (fix/the-controller-may-publish-memberships)
|
||||||
|
amended-design: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# 183 — The controller could not publish the memberships it issued
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
The controller release that issues a membership per assignment ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md))
|
||||||
|
rolled onto the control node. After its first push the controller's log said, once:
|
||||||
|
|
||||||
|
```
|
||||||
|
nats: permissions violation: Permissions Violation for Publish to "mesh.assignment.<node>.<module>"
|
||||||
|
```
|
||||||
|
|
||||||
|
No membership reached the assignments stream. Nothing else changed: every runtime kept serving the
|
||||||
|
shape it derives for itself, which is what the decision says happens while no membership is issued,
|
||||||
|
and so the mesh looked healthy while the whole new mechanism was inert.
|
||||||
|
|
||||||
|
## Why this is here
|
||||||
|
|
||||||
|
The controller composes every account's grant, its own included, and its own grant named the
|
||||||
|
control, node and JetStream subjects and not the assignments it alone issues. A grant composed by its
|
||||||
|
holder is checked by nothing but the server at publish time, and a refused publish is one log line
|
||||||
|
that nothing reads. The fallback that makes the roll-out safe is the same thing that makes this
|
||||||
|
failure silent.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01
|
||||||
|
|
||||||
|
The controller's grant names `mesh.assignment.>`; the broker golden changed by that one line. A
|
||||||
|
grant composed by its holder arrives late — the broker node is pushed after the controller rolls —
|
||||||
|
so the roll-out is merge, push the broker node, then any push issues memberships.
|
||||||
|
|
||||||
|
The refusal had a second consequence: published with the daemon's own context, the refused
|
||||||
|
membership was waited on for ever and the controller went deaf —
|
||||||
|
[issue 185](../185-a-refused-membership-publish-stops-the-controller/00-report.md).
|
||||||
|
|
||||||
|
*How it is checked:* the broker golden carries the allow line; live, a runtime's log after the next
|
||||||
|
push says it was issued a membership rather than that none exists.
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-10-01
|
||||||
|
located-in: [mesh-controller cmd/mesh-controller/upgrades.go (the merge handler builds inside the receive loop)]
|
||||||
|
fixed-by: mesh-controller PR 197 (ADR 0162: the merge handler writes a plan, asks the first tier and returns; outcomes and a ticker advance it) and PR 199
|
||||||
|
amended-design: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# 184 — A merge announcement blocks the controller's receive loop
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
The controller restarted at 12:53:32Z on 2026-10-01 and heard, among its first messages, a merge
|
||||||
|
announcement for the catalogue that touched some forty modules. Until 13:17:16Z — twenty-four
|
||||||
|
minutes — it took nothing else in: build outcomes that the build machine had announced and that
|
||||||
|
the console's `builds` log showed as done sat unrecorded, so `builds` listed none of them and the
|
||||||
|
modules stayed at their old versions; the node heartbeats were dropped by the bus as a slow consumer
|
||||||
|
on `mesh.control.*.alive`, twice. When the merge handler returned, everything queued arrived at once
|
||||||
|
and was taken in within a second.
|
||||||
|
|
||||||
|
## Why this is here
|
||||||
|
|
||||||
|
The receive loop acts on one message at a time, which is the right discipline for a store the
|
||||||
|
controller must write in order. Acting on a merge means asking builds and waiting for each outcome,
|
||||||
|
minutes of work, and that wait happens inside the loop that would otherwise be hearing the outcomes
|
||||||
|
of everything else. The bus keeps the merge message alive while the work runs — the fix for the
|
||||||
|
earlier repeated-merge fault — so nothing is lost and nothing is redelivered, and nothing is heard
|
||||||
|
either. The design permits the controller to go deaf for as long as a merge takes to build, and no
|
||||||
|
status says so: the mesh reads as quiet, builds read as missing, and heartbeats read as a slow
|
||||||
|
machine.
|
||||||
|
|
||||||
|
## Also seen, 2026-10-01 evening
|
||||||
|
|
||||||
|
The handler's work is lost when the controller is replaced while it runs. The runtime image's merge
|
||||||
|
at 14:22Z was taken by a controller that rolled forty seconds later, after asking the image's own
|
||||||
|
build and before asking the forty-two that stand on it. The announcement was redelivered to the new
|
||||||
|
controller, which judged it history — the source had been looked at after the merge — and said
|
||||||
|
"nothing the mesh holds reads it". The dependents were asked by hand. Whatever shape the fix takes,
|
||||||
|
the work a merge implies has to be recorded as asked, not held in the handler's stack.
|
||||||
|
|
||||||
|
## What a fix needs to decide
|
||||||
|
|
||||||
|
Whether a merge is work the loop dispatches and returns from — the builds asked, the outcomes taken
|
||||||
|
in by the same `Built` handler every other outcome uses, since the stream already delivers them —
|
||||||
|
or whether the loop runs more than one handler at a time with the store's ordering kept for the
|
||||||
|
kinds that need it. The first is smaller and keeps one ordering. Either way, a controller that is
|
||||||
|
busy should say so where `status` is read.
|
||||||
|
|
||||||
|
*How this would be checked:* a controller test where a merge announcement that asks a slow build
|
||||||
|
and a build outcome for another module arrive together, and the outcome is recorded before the
|
||||||
|
build finishes; live, the controller's log during the next catalogue merge shows registrations
|
||||||
|
interleaved with the merge's own.
|
||||||
|
|
||||||
|
## Decided, 2026-10-01
|
||||||
|
|
||||||
|
[ADR 0162](../../02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md): a merge
|
||||||
|
produces a tiered plan the store keeps; the handler asks the first tier and returns; outcomes advance
|
||||||
|
the plan; a controller replaced mid-plan resumes it. The loop is never held by a build again.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01 evening
|
||||||
|
|
||||||
|
Since the controller holding ADR 0162's plan rolled, a merge announcement is handled in
|
||||||
|
milliseconds: the plan is written, the first tier asked, the loop free. The builds the merge
|
||||||
|
implies are asked from the store's record, tier by tier, so a controller replaced mid-plan resumes
|
||||||
|
it rather than losing it. The first live merge under it (a controller change) is the proof the
|
||||||
|
decision's table asks for; its tiers are read with `plans`.
|
||||||
|
|
||||||
|
*How it is checked:* the plan tests in mesh-controller; live, `plans` after a merge and the loop's
|
||||||
|
log taking reports in while the plan builds.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user