Compare commits
178
Commits
6c2d5f5913
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e11bf320c9 | ||
|
|
da8b4b4ee4 | ||
|
|
c026d5221e | ||
|
|
983fd412c6 | ||
|
|
9ac2493e2c | ||
|
|
560f25c2c7 | ||
|
|
9e0288128b | ||
|
|
709240ec1f | ||
|
|
d57289e049 | ||
|
|
d4a2f99ab5 | ||
|
|
a9f91fdd0c | ||
|
|
131a5e4714 | ||
|
|
329a24fdae | ||
|
|
7f72f3b79a | ||
|
|
114a71f36f | ||
|
|
0f407417f3 | ||
|
|
4af731df19 | ||
|
|
7b1dabbce0 | ||
|
|
2caa5e827b | ||
|
|
179fd7f83f | ||
|
|
d0d5799884 | ||
|
|
9ba4de5557 | ||
|
|
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 | ||
|
|
37b46d5349 | ||
|
|
16855ade02 | ||
|
|
8dd566c0aa | ||
|
|
a98ee0f529 | ||
|
|
ad4a5ea004 | ||
|
|
0cf1ad5dad | ||
|
|
69a002fce3 | ||
|
|
16a1a52cd8 | ||
|
|
af170e3a67 | ||
|
|
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:
|
||||||
|
|||||||
+40
-2
@@ -9,6 +9,11 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
|
|||||||
|
|
||||||
- **node** — a machine in the mesh. There are 0..n of them, and each runs the host agent. A node is
|
- **node** — a machine in the mesh. There are 0..n of them, and each runs the host agent. A node is
|
||||||
just a machine that has joined; being one implies nothing about what it runs.
|
just a machine that has joined; being one implies nothing about what it runs.
|
||||||
|
- **operator account** — the login name of the person who works on a node, stated on the node
|
||||||
|
record; empty for a machine nobody logs into. Everything the mesh places under a person's home is
|
||||||
|
resolved against this account's home and owned by it
|
||||||
|
([ADR 0181](../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)).
|
||||||
|
Not "the user" (ambiguous with a module's own account) and not a name a definition carries.
|
||||||
- **control-node** — the one node that also holds the `mesh-controller` seat. There is exactly one
|
- **control-node** — the one node that also holds the `mesh-controller` seat. There is exactly one
|
||||||
per mesh. "control-node" is not a separate kind of machine — it is a node that additionally runs
|
per mesh. "control-node" is not a separate kind of machine — it is a node that additionally runs
|
||||||
the controller (and, today, the foundation). Lose it and the other nodes keep running what they
|
the controller (and, today, the foundation). Lose it and the other nodes keep running what they
|
||||||
@@ -52,8 +57,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
|
||||||
@@ -75,8 +83,38 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
|
|||||||
is a role you occupy, and the two meet where a seat delivers a provision: occupying the seat is
|
is a role you occupy, and the two meet where a seat delivers a provision: occupying the seat is
|
||||||
what makes a module *the* provider of it.
|
what makes a module *the* provider of it.
|
||||||
|
|
||||||
|
## The surfaces
|
||||||
|
|
||||||
|
- **console** — the module (`mesh-console`) that puts the mesh's tools in front of whoever is on a
|
||||||
|
machine: an MCP endpoint on the machine's loopback for an agent, the same endpoint for a person. It
|
||||||
|
is assigned like any module, holds a credential the mesh minted, and calls tools under a grant its
|
||||||
|
manifest declares (`invokes`). Loopback is the authority boundary: whoever is on the machine owns the
|
||||||
|
mesh there ([ADR 0152](../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
|
||||||
|
Not "the tool bridge", "the brain" or "the MCP server" — those name the predecessor's program or a
|
||||||
|
protocol, and the console is a module.
|
||||||
|
- **invokes** — the manifest word for the tools a module calls, `<module>.<tool>` each or `*` for
|
||||||
|
every one. A grant on the publish side and nothing else; a module that declares none calls nothing.
|
||||||
|
|
||||||
## How this page is kept
|
## How this page is kept
|
||||||
|
|
||||||
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 own code is built into — its tools, a seat's implementation, a daemon — in any language the mesh has a toolchain for, interpreted or compiled; never an image. One module may declare several ([ADR 0188](../02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)).
|
||||||
|
- **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.
|
||||||
@@ -72,6 +72,12 @@ answers from it. Nothing flows back: this repository is public, the mesh is not,
|
|||||||
would be how installation-specific detail arrives into documents that must not carry it
|
would be how installation-specific detail arrives into documents that must not carry it
|
||||||
([`README.md`](../README.md)).
|
([`README.md`](../README.md)).
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-09-30, by ADR 0153.** What stands: read where it is written, no copy,
|
||||||
|
> one-way. What moved: the reader is a module the mesh assigns (`records`, a checkout at a commit every
|
||||||
|
> answer names) rather than the agent session of design 15, which is not built; and "the search consults
|
||||||
|
> the agent" has no store to consult since the cut-over — the console's tool list is where the record
|
||||||
|
> appears beside everything else. [ADR 0153](0153-the-record-is-read-by-a-module-and-the-console-lists-it.md).
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
**This repository stops being a fourth knowledge system, properly.** The original objection was
|
**This repository stops being a fourth knowledge system, properly.** The original objection was
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ reconstructed: false
|
|||||||
|
|
||||||
# 39. What the SDK holds, and what it refuses
|
# 39. What the SDK holds, and what it refuses
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-02, by [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md).** The test of this record — frequent *and* cascading does not belong — stands and now applies to one SDK per language. Where *what it holds* names the broker client and the event consumer, read: the local protocol a tools bundle speaks to the node's runtime; the transport lives in the runtime and in no SDK, which is what keeps a bus change from rebuilding any module in any language.
|
||||||
|
|
||||||
_Reconciliation note (2026-09-05): supersedes the earlier "repository structure" decision, which the consolidation folded; no standalone record remains to point at, so body references to it now point at the nearest surviving record, [ADR 0015](0015-applications-live-in-their-own-repository.md)._
|
_Reconciliation note (2026-09-05): supersedes the earlier "repository structure" decision, which the consolidation folded; no standalone record remains to point at, so body references to it now point at the nearest surviving record, [ADR 0015](0015-applications-live-in-their-own-repository.md)._
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -204,3 +204,14 @@ only, the refresh token only, the manager node only.**
|
|||||||
record extends, amended to describe the adapter generalisation.
|
record extends, amended to describe the adapter generalisation.
|
||||||
- The read-only vendor-agnostic analysis, 2026-09-05 (code workspace) — the inventory and the decisions
|
- The read-only vendor-agnostic analysis, 2026-09-05 (code workspace) — the inventory and the decisions
|
||||||
taken on the open questions this record encodes.
|
taken on the open questions this record encodes.
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-02, by [ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md).**
|
||||||
|
> What stands: `model-access` is one vendor-blind provision for the consumers that do not care which
|
||||||
|
> vendor answers; a vendor's lifecycle is an adapter's; the carve-out that one node holds a refreshable
|
||||||
|
> grant's refresh token readably. What moved: the Anthropic adapter is no longer a part of the
|
||||||
|
> controller's licences context but a module, `claude-licence-manager`, holding the seat
|
||||||
|
> `anthropic-licence-manager`, with the grants in its own store encrypted with a key the vault made for
|
||||||
|
> it; and the agent at a terminal is not a consumer of `model-access` — it is coupled to an Anthropic
|
||||||
|
> grant and uses the seat. The consequence above that the three binding columns *become three ordinary
|
||||||
|
> consumers of `model-access`* therefore no longer describes the agent's bindings; they are the
|
||||||
|
> manager's. The static-key adapters and the vendor-blind records stay where this record put them.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ Anything with the control plane in reach can ask any module anything it serves.
|
|||||||
harder: nothing outside the control plane can, and the control plane's connection is one more
|
harder: nothing outside the control plane can, and the control plane's connection is one more
|
||||||
thing on the path of every question — a cost accepted for the audit it buys.
|
thing on the path of every question — a cost accepted for the audit it buys.
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-09-30, by ADR 0152.** What stands: `ask` on the control plane, and
|
||||||
|
> that every call passes an account whose permission list says what it may ask. What moved: "nothing
|
||||||
|
> outside the control plane can" stopped being true when a person's account gained a publish grant per
|
||||||
|
> tool (design 25 §7, 2026-09-28), and [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)
|
||||||
|
> takes the first option above for a module as well — a manifest declares `invokes`, and the bus grants
|
||||||
|
> exactly that publish side. The audit the second option bought is the bus's permission list, which
|
||||||
|
> derives both.
|
||||||
|
|
||||||
## How it is checked
|
## How it is checked
|
||||||
|
|
||||||
A tools-only bed asks a served tool through the control plane and asserts an answer arrived —
|
A tools-only bed asks a served tool through the control plane and asserts an answer arrived —
|
||||||
|
|||||||
@@ -283,3 +283,13 @@ modules in the catalogue require it — so a shared secret is a requirement answ
|
|||||||
which is what this record asks for. Private keys are still made where they are used and never
|
which is what this record asks for. Private keys are still made where they are used and never
|
||||||
travel, which is the other half and was never in question.
|
travel, which is the other half and was never in question.
|
||||||
|
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-02, by [ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md).**
|
||||||
|
> What stands: every shared secret the mesh makes is the vault's, a private key is made where it is
|
||||||
|
> used, and a long-lived value a backend issues enters the vault's custody — here as the key the vault
|
||||||
|
> makes for the licence manager, which encrypts the vendor's grants at rest with it. What this record
|
||||||
|
> did not foresee: a credential that lives hours, issued by a vendor to the one module that holds its
|
||||||
|
> grant, and handed by that module to the agent on each node sealed to that node's module key, on
|
||||||
|
> request/reply over the bus, never through the vault and never as a file the host writes. ADR 0183
|
||||||
|
> states that as a bounded exception — one vendor, tokens that live hours, one recipient per message —
|
||||||
|
> and a second such channel is a decision of its own.
|
||||||
|
|||||||
@@ -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.**
|
||||||
|
|||||||
@@ -150,6 +150,16 @@ closed by this record, only answered by it.
|
|||||||
resolvable inside the mesh — holds unchanged and by the same means the machine already uses.
|
resolvable inside the mesh — holds unchanged and by the same means the machine already uses.
|
||||||
- **Issue 110 stops being a container-DNS inconvenience and becomes a prerequisite** for the mesh not
|
- **Issue 110 stops being a container-DNS inconvenience and becomes a prerequisite** for the mesh not
|
||||||
restarting itself whenever it learns a name.
|
restarting itself whenever it learns a name.
|
||||||
|
- **A container that names a resolver of its own has opted out of the machine's**, and the copy this
|
||||||
|
record removes was the only reason such a container could reach anything by a mesh name.
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-09-30, the afternoon this landed. Found the hard way.** The mail
|
||||||
|
> system's admin, behind Mailu's own resolver, lost its database the moment the copy went
|
||||||
|
> ([issue 171](../04-ISSUES/171-a-modules-own-resolver-knows-no-mesh-name/00-report.md)). A `dns` on
|
||||||
|
> a container is a decision about whether mesh names exist inside it, not a preference; the module
|
||||||
|
> was corrected, and whether the controller should refuse the contradiction is that issue's open
|
||||||
|
> question.
|
||||||
|
|
||||||
- **A container started by hand gets the mesh's names too**, where before only declared containers did.
|
- **A container started by hand gets the mesh's names too**, where before only declared containers did.
|
||||||
Design 08 drew that boundary deliberately, on the grounds that reaching into every container is what
|
Design 08 drew that boundary deliberately, on the grounds that reaching into every container is what
|
||||||
a nameserver would be for. This record accepts that consequence rather than working around it: a
|
a nameserver would be for. This record accepts that consequence rather than working around it: a
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ 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
|
||||||
|
|
||||||
|
> **Widened — 2026-10-02, by [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md).** A module's long-lived process is a bundle in any language the mesh has a toolchain for, run as a unit the host writes; this record never said one language and never meant one, and 0188 says so as the rule.
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** For a module's *tools*, read that record: one runtime per node, the node's one account, bundles loaded from the memberships. This record still governs a module's long-lived processes — a daemon, a provisioner, a scheduled ingest — and the account invariant for them.
|
||||||
|
|
||||||
## Context
|
## 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
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-30
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 152. The operator's surface is a module the mesh assigns: the console
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
**Since the bus moved, nobody can ask the mesh anything without opening a shell on a machine.** Every
|
||||||
|
tool call an operator's assistant makes fails, on every machine including the one the operator sits
|
||||||
|
at, with *AMQP not connected*
|
||||||
|
([issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
|
||||||
|
The program answering is the predecessor's tool server, started on the workstation by hand, with the
|
||||||
|
predecessor's broker address written into the assistant's own configuration. It has no manifest, no
|
||||||
|
assignment, no account on the bus, and the mesh has never known it exists. Nothing regressed: the
|
||||||
|
mesh removed a transport that a program outside the mesh still dials.
|
||||||
|
|
||||||
|
**The mesh has a tool model, and it works.** A module serves each tool on its own subject and its
|
||||||
|
account may serve nothing else ([ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md));
|
||||||
|
a person is issued an account whose only permission is to publish the tool subjects named at issue
|
||||||
|
([25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §7); a client on the
|
||||||
|
runtime repository's main branch speaks that account as a command line and as an MCP server. Measured
|
||||||
|
on the live mesh on 2026-09-28: a call to the forge's `gitea_list_repos` answered with real
|
||||||
|
repositories; the tool list came back empty, because it asks the catalogue for a tool nothing serves
|
||||||
|
([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
|
||||||
|
|
||||||
|
**Two records have already said where the surface belongs.** ADR 0132's consequences: *the MCP
|
||||||
|
surface belongs inside the mesh — a module the mesh assigns to the machine where the agent sits, with
|
||||||
|
a credential the mesh minted and authority derived from what it may call, not a program started by
|
||||||
|
hand with a credential printed to a terminal.* Design [33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)
|
||||||
|
§6 says the same. Neither is a decision about the surface: 0132 decided where a seat's tools live,
|
||||||
|
and named the surface in passing.
|
||||||
|
|
||||||
|
**And one record says the opposite, in the letter.** [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md)
|
||||||
|
made the control plane *the* way to ask a module, deferred "calling is a grant" until something asked
|
||||||
|
for it, and recorded that *nothing outside the control plane can*. A person's account (design 25 §7,
|
||||||
|
built 2026-09-28) is exactly that grant, minted for a person. So 0095's exclusivity has already been
|
||||||
|
widened once without a record saying so; a module that calls tools widens it a second time, and this
|
||||||
|
record is where that is said.
|
||||||
|
|
||||||
|
**What a module's account may do today, counted from the composition** (`internal/broker`): publish
|
||||||
|
its own events, publish the accept subjects of seats it uses, subscribe its own tools and what it
|
||||||
|
consumes. No module principal may publish another module's tool subject. Of 72 modules in the
|
||||||
|
catalogue, 45 serve tools and 0 may call one.
|
||||||
|
|
||||||
|
The question the work order asks before any code: **does the mesh grow its own operator surface, or is
|
||||||
|
the surface an ordinary module that happens to serve tools?**
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**1. The control plane serves the agent protocol itself** — a listener on the controller, or a verb
|
||||||
|
its binary runs. Rejected. It puts a surface for tools the control plane does not implement on the one
|
||||||
|
component that must stay answerable while it is itself being replaced, which is the reason 0132
|
||||||
|
rejected the control plane as the answer to discovery. A person on a workstation would reach it over
|
||||||
|
the network, and the networked surface [ADR 0035](0035-one-implementation-several-surfaces.md)
|
||||||
|
reserves for that authenticates through an OAuth2 provider that is not configured — so the controller's
|
||||||
|
`api` verb correctly serves nothing today, and this option would either wait for it or bypass it.
|
||||||
|
|
||||||
|
**2. A program a person installs and starts by hand with a printed credential** — what exists on the
|
||||||
|
runtime repository's main. Rejected as the end state. It is outside the mesh in every way issue 147
|
||||||
|
names: no assignment, no declaration, no seat, no check that it reaches anything, revoked only by a
|
||||||
|
person remembering to. It is the predecessor's arrangement one bus later, and it fails the same way
|
||||||
|
the next time an address moves. It stays as the recovery path, the way the command line does
|
||||||
|
(ADR 0035): a credential from `operator issue` and the `mesh` client work with no console assigned.
|
||||||
|
|
||||||
|
**3. An ordinary module, assigned to the machine the person sits at, holding a credential the mesh
|
||||||
|
minted, serving the mesh's tools on that machine's loopback.** Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**The console is a module.** `mesh-console` is built by the mesh, registered like any module,
|
||||||
|
assigned to a machine, and given a bus credential sealed to that machine. It serves the mesh's tools to
|
||||||
|
whoever is on that machine: to an agent over MCP, and to a person through the same endpoint. Assigning
|
||||||
|
it to a machine is what makes the mesh reachable from there; unassigning it revokes that, at the next
|
||||||
|
composition, with nothing on the machine to remember to remove.
|
||||||
|
|
||||||
|
**A grant to call is a manifest word: `invokes`.** A module declares the tools it calls, each as
|
||||||
|
`<module>.<tool>`, or the single entry `*` for every tool on the mesh. The bus grants exactly that
|
||||||
|
publish side and nothing beside it — no event, no subscription, no seat. This is ADR 0095's deferred
|
||||||
|
first option, taken now that a consumer asks for it; a person's account already has this shape, and
|
||||||
|
the same composition derives both. The control plane's `ask` stands, and 0095's audit point with it:
|
||||||
|
every call still passes one account whose permission list says what it may ask.
|
||||||
|
|
||||||
|
**Authority is the machine's login.** The console listens on the machine's loopback only, declared
|
||||||
|
`from: machine`, so whoever can open a socket on the machine is whoever owns the machine, and *the
|
||||||
|
account that installed the host owns the mesh on that node*
|
||||||
|
([ADR 0034](0034-the-local-account-owns-the-mesh.md)). Anything on a machine may call anything on it,
|
||||||
|
and that is the whole of local ([ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md)).
|
||||||
|
The mesh knows no person: what the audit sees is which console asked, under the account
|
||||||
|
`<node>.mesh-console`. How a person's identity reaches a session is the question design 15 leaves
|
||||||
|
open, and this record does not close it.
|
||||||
|
|
||||||
|
**What the console lists is asked of the modules.** Design 33 §5: a module's own tools are answered by
|
||||||
|
the module, from the code that defines them. The tool runtime therefore answers one reserved verb for
|
||||||
|
every module it serves — `tools`, the module's tool names, descriptions and argument schemas — and the
|
||||||
|
console assembles its list by asking the catalogue which modules the mesh holds and each module what
|
||||||
|
it answers. A module that is not running is absent from the list and says so; a tool an agent already
|
||||||
|
knows the name of can be called whether or not it was listed. A module may not name a tool of its own
|
||||||
|
`tools`; the runtime refuses the collision at load rather than letting one shadow the other. A
|
||||||
|
role's tools, and the mesh's own verbs, join the list when the `mesh-controller` seat serves them
|
||||||
|
(design 33 §1, third family) — the console reads whatever the mesh can say about itself, and grows as
|
||||||
|
that does.
|
||||||
|
|
||||||
|
**The console holds one credential and one grant: `*`.** It is the operator's surface on a machine the
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
- **The way a person drives the mesh is inside the mesh.** It is declared, delivered, replaced and
|
||||||
|
revoked by the same machinery as everything else, and `status` says whether the machine carrying it
|
||||||
|
has applied. Issue 147's shape — a surface kept alive by an address in a file — cannot recur, because
|
||||||
|
there is no file: the console's credential names the bus the mesh is on, and moves when it does.
|
||||||
|
- **A module may now call tools, which ADR 0095 had reserved to the control plane.** The grant is
|
||||||
|
explicit, per tool or `*`, and derived by the same composition that grants everything else. A module
|
||||||
|
that declares no `invokes` gains nothing. What got harder: a manifest reviewer has one more field to
|
||||||
|
read for authority, and `*` in it deserves the reader's attention every time.
|
||||||
|
- **A new manifest word ships one release before any manifest uses it**, and must reach both parsers:
|
||||||
|
the build machine's and the running controller's. The console's manifest cannot be registered until
|
||||||
|
the controller and the builder that packages it have been rebuilt with the word.
|
||||||
|
- **Discovery costs a fan-out per list.** One request per module the mesh holds, answered at once by
|
||||||
|
the bus for every module nothing serves, so the cost is bounded by the modules that are up. The list
|
||||||
|
is cached briefly in the console; a module assigned a moment ago appears at the next refresh.
|
||||||
|
- **Every tool runtime must be rebuilt once** to answer `tools`. Until a module is, it is callable and
|
||||||
|
not listed, and the console says which modules did not answer.
|
||||||
|
- **The mesh's own verbs are not in the console yet.** `status`, `push`, `assign` are the
|
||||||
|
`mesh-controller` seat's tools under 0132, and the three prerequisites 0132 names are still not in
|
||||||
|
place. A person asking what a node runs still opens a shell for that question, and that gap is design
|
||||||
|
33's to close, not this record's — recorded here so nobody reads the console as the whole of 147.
|
||||||
|
- **The person's credential is not retired.** `operator issue` and the `mesh` client remain the path
|
||||||
|
when no console is assigned, and the path an operator uses to bring a mesh up far enough to assign
|
||||||
|
one.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A module's `invokes` becomes exactly that publish grant, and nothing else | the bus user composition test: a module invoking `shop.price` may publish that subject and no other tool's; `*` may publish every tool subject; neither may publish an event or subscribe anything it did not consume |
|
||||||
|
| A malformed `invokes` entry is refused at registration | a parser test: an entry naming no tool is a problem named in the manifest's words |
|
||||||
|
| The runtime answers `tools` for every module it serves | the runtime's test: a module registering two tools answers three names, and a module naming one of its own `tools` is refused at load |
|
||||||
|
| The console's list is what the modules answer | the client's test against a real bus: two modules up, a third the catalogue holds and nothing serves, and the list carries the two and names the third as not answering |
|
||||||
|
| A call from the console reaches a module over the bus | the same test, and the live mesh: the console assigned to a workstation answers `tools/list` on its loopback and a call to the forge returns repositories |
|
||||||
|
| The console listens on loopback and nowhere else | its manifest declares `from: machine`, and the filter composed for the machine opens nothing for it |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md) — the surface outside the mesh
|
||||||
|
- [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md) — extended: a grant to call, for a module as for a person
|
||||||
|
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — where a seat's tools live, and the sentence that named the surface
|
||||||
|
- [ADR 0035](0035-one-implementation-several-surfaces.md) — three surfaces over one implementation
|
||||||
|
- [ADR 0034](0034-the-local-account-owns-the-mesh.md), [ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md) — why loopback is the authority boundary
|
||||||
|
- [33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §5, §6 — discovery, and what serves it to an agent
|
||||||
|
- [34 — The console](../03-DESIGN/01-to-be/34-the-console.md) — the design this record authorises
|
||||||
|
- mesh-tools `src/client.ts`, `src/mcp.ts`, `src/mesh.ts` — the client this makes a module of
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
---
|
||||||
|
topic: how we work
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-30
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 153. The record is read by a module the mesh assigns, and the console lists it
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0025](0025-the-design-record-is-read-not-copied.md) decided that this repository is **read where
|
||||||
|
it is written, never copied to be found**: an agent reads it directly, and a search of the mesh's
|
||||||
|
memory consults that agent so its answers appear beside ordinary results. It named the check that
|
||||||
|
closes [issue 006](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md): search
|
||||||
|
for a phrase that appears only in a design document here, and get it back. It gated the build on an
|
||||||
|
agent that did not exist — the mesh session of
|
||||||
|
[15 — The agent session](../03-DESIGN/01-to-be/15-the-agent-session.md) — and on a search that
|
||||||
|
does not exist either, now: the knowledge base 0025 meant was the predecessor's, and since the
|
||||||
|
cut-over nothing reaches it ([issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
|
||||||
|
|
||||||
|
**So the two halves of 0025 have no home.** There is no store to be "beside", and no session to be
|
||||||
|
the reader. What the mesh has instead, since today: a tool model in which every module answers what
|
||||||
|
it serves, and a console on the machine a person sits at that lists every tool the running modules
|
||||||
|
answer ([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)). An agent holding the
|
||||||
|
console does not search a store; it reads a tool list and calls what fits the question.
|
||||||
|
|
||||||
|
**What 0025 could not tolerate was a derived copy** — the enforced copy winning while the reasoned one
|
||||||
|
quietly stops being true. It rejected a sync for that reason and for no other. A git checkout is not a
|
||||||
|
derived copy: it is the same bytes at a commit the answer names, and the only way it can differ from
|
||||||
|
the source is by lagging behind it, which is measurable and stated. 0025's own words allow it —
|
||||||
|
*retrieval is an agent reading this repository, not a copy living in a second store* — and the
|
||||||
|
transformation that makes a copy dangerous is exactly what a checkout does not do.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**1. Wait for the mesh session.** Rejected. Design 15 is `designed` with nothing built, its model
|
||||||
|
access is a provisions question with no consumer identity yet, and 006 has waited since 2026-08-23.
|
||||||
|
A record whose check cannot run is a rule enforced by nothing.
|
||||||
|
|
||||||
|
**2. The console reads the repository itself.** Rejected. The console holds nothing and decides
|
||||||
|
nothing (ADR 0152, ADR 0035); a reader inside it would be a second implementation of a thing that
|
||||||
|
should be one module, unavailable to a person's client and to any other module.
|
||||||
|
|
||||||
|
**3. A module that keeps a checkout of the repository and answers questions about it, listed by the
|
||||||
|
console like any tool.** Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**The reader is a module: `records`.** It requires the `git` provision — the forge — clones the
|
||||||
|
repository its settings name, keeps the checkout current on every merge the forge announces and on a
|
||||||
|
timer, and answers over the bus: where a phrase appears as written (document, line, nearest heading),
|
||||||
|
one document whole, what a folder holds, and where the checkout stands — always with the commit it
|
||||||
|
read. Nothing is indexed, ranked or summarised: a design record is found by its own words, and a
|
||||||
|
reader deciding which words matter would be a second opinion about somebody else's document.
|
||||||
|
|
||||||
|
**The repository is a setting, not a manifest field.** The module names no mesh
|
||||||
|
([ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md)): which repository it reads is
|
||||||
|
the assignment's business, and an installation that keeps its record elsewhere sets that. Until a
|
||||||
|
repository is set it serves no tools and says why. Public repositories only; it asks for no
|
||||||
|
credential, because a secret it did not need would be one more thing to seal.
|
||||||
|
|
||||||
|
**"Beside everything else" is the console's tool list.** 0025's second half — the search consults the
|
||||||
|
agent — has no store to consult and needs none: the console lists `records_search` beside the forge's
|
||||||
|
tools and the mesh's own verbs, with a description that says when to call it, and an agent choosing
|
||||||
|
tools for a symptom is the search. That is surfacing, not merely reaching: nobody has to know this
|
||||||
|
repository exists to be offered it.
|
||||||
|
|
||||||
|
**Reading stays one-way.** The module reads the forge and answers; nothing flows back into the
|
||||||
|
repository. It holds no credential that could write.
|
||||||
|
|
||||||
|
**The mesh session, when it exists, is a caller of this module, not a replacement for it.** Design 15's
|
||||||
|
*it holds the design record by reading it* is satisfied by asking `records`; the session brings
|
||||||
|
judgement, this brings the text.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Issue 006 closes on 0025's own check**, run through the console: `records_search` for a phrase
|
||||||
|
that appears in one design document here returns that document. The module's test does the same
|
||||||
|
against a repository it makes.
|
||||||
|
- **The as-is knowledge document is rewritten.** [`07-knowledge.md`](../03-DESIGN/00-as-is/07-knowledge.md)
|
||||||
|
described the predecessor's two stores; neither is reachable from the mesh, and what the mesh knows
|
||||||
|
is now what its modules answer. Saying otherwise is the failure this repository exists to name.
|
||||||
|
- **A checkout lags.** Between a merge and the next sync — seconds when the forge announces it,
|
||||||
|
minutes when it does not — an answer is the previous commit's, and says which. That is the cost of
|
||||||
|
no copy, and it is a number rather than a silence.
|
||||||
|
- **The reader depends on the forge module's event, by name.** `consumes: gitea.pull.merged` names a
|
||||||
|
module rather than the `git` seat, because the seat declares no events. A forge that is not gitea
|
||||||
|
leaves the timer as the only refresh, which still works.
|
||||||
|
- **What got harder:** the record is now reachable from every machine holding a console, which is what
|
||||||
|
was wanted, and a reader must remember that this repository is public and the mesh is not — the
|
||||||
|
module reads the public repository and nothing about the installation.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A phrase in one document comes back from where it is written, with the commit | the module's test against a repository it makes; and live, through the console |
|
||||||
|
| A merge on the origin is pulled and the next answer names the new commit | the same test |
|
||||||
|
| A path outside the checkout is refused, not resolved | a test per shape |
|
||||||
|
| A failed sync leaves the checkout standing and is said | a test against an unreachable origin |
|
||||||
|
| Without a repository set, no tools are served and the log says why | the module's own start |
|
||||||
|
| The console lists `records_search` beside every other tool | the console's listing, live |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0025](0025-the-design-record-is-read-not-copied.md) — extended: the reader is a module, the search is the console's list
|
||||||
|
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — what lists it
|
||||||
|
- [issue 006](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md) — what closes
|
||||||
|
- [35 — Reading the record](../03-DESIGN/01-to-be/35-reading-the-record.md) — the design
|
||||||
|
- mesh-catalog `modules/records` — the module (PR 183)
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-30
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 154. The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) decided that a seat's protocol
|
||||||
|
carries its tools in full, that holding a seat means serving them, and that the mesh's own verbs are
|
||||||
|
the `mesh-controller` seat's. It named three prerequisites, none in place: the protocol in the store
|
||||||
|
rather than in compiled defaults; a protocol richer than a list of verbs; a node-scoped seat's subject
|
||||||
|
carrying the node. And it left one thing to a decision per seat: **which verbs each seat serves**,
|
||||||
|
because a seat's tools bind every future holder.
|
||||||
|
|
||||||
|
The console shipped the same day ([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md))
|
||||||
|
and made the gap visible from the operator's chair: a person on a workstation could call every tool a
|
||||||
|
*module* serves and none of the mesh's own. What a node runs, what is assigned, whether a push
|
||||||
|
applied — the questions issue 147 opened with — still meant a shell on the control node. The console's
|
||||||
|
own handshake said so.
|
||||||
|
|
||||||
|
The control plane already answers every one of those questions, as commands: `status --json`,
|
||||||
|
`node show`, `plan --json`, `assign`, `push`. [ADR 0035](0035-one-implementation-several-surfaces.md)
|
||||||
|
says a surface is an adapter over those with no decisions in it, and the `api` verb proves the shape:
|
||||||
|
every route calls the function the command line calls.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**1. Leave the mesh's verbs to the shell until an identity provider authenticates the HTTP API.**
|
||||||
|
Rejected. The authenticated network surface is for a browser on another machine; the console is
|
||||||
|
already behind the machine's login (0152), and the bus already carries every other tool call under an
|
||||||
|
account whose permission list says what it may ask. Waiting would keep the one surface the mesh has
|
||||||
|
from answering the mesh's own questions, for a reason that does not apply to it.
|
||||||
|
|
||||||
|
**2. Serve the verbs as the mesh-controller *module's* tools, `mesh.mod.mesh-controller.tool.<verb>`.**
|
||||||
|
Rejected; 0132 rejected it already. The controller holds a seat, and the verbs must keep their address
|
||||||
|
while the control plane is being replaced, which is the moment they are most needed. A module's name
|
||||||
|
would change with the implementation; the seat's does not.
|
||||||
|
|
||||||
|
**3. Call each command's function inside the serving process.** Rejected on two facts: the commands
|
||||||
|
print, to the process's standard output, and two calls answered at once would read each other's
|
||||||
|
words; and each command opens and closes its own stores, which the serving process holds open. Making
|
||||||
|
every command return a value is the larger refactor, and it would give the tools a second code path to
|
||||||
|
keep in step with the command line — the thing ADR 0035 forbids.
|
||||||
|
|
||||||
|
**4. The holder of the seat runs the command it names, in its own binary, and answers what it
|
||||||
|
printed.** Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**The `mesh-controller` seat serves twelve verbs**, and these are its interface, additive within a
|
||||||
|
version ([33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §7):
|
||||||
|
|
||||||
|
| verb | answers with | takes |
|
||||||
|
|---|---|---|
|
||||||
|
| `tools` | every seat's tools, from the mesh's records | nothing |
|
||||||
|
| `status` | what is wrong, quiet, behind, waiting — `status --json` | nothing |
|
||||||
|
| `nodes` | every machine and its mode | nothing |
|
||||||
|
| `node` | what one machine reported, what it is assigned, why | `node` |
|
||||||
|
| `modules` | every module, its version, commit and machines | nothing |
|
||||||
|
| `seats` | every seat, what it delivers, who holds it — `seats --json` | nothing |
|
||||||
|
| `builds` | what was built lately and what came of it | `module` (optional) |
|
||||||
|
| `plan` | the declaration a machine would be sent — `plan --json` | `node` |
|
||||||
|
| `assign`, `unassign` | the mesh's own words, refusal included | `node`, `module` |
|
||||||
|
| `push` | that it was sent; `status` says what the machine did | `node` (optional: every machine behind) |
|
||||||
|
| `build` | that the build machine was asked; `builds` says what came of it | `repository`, `path`, `ref` |
|
||||||
|
|
||||||
|
**Each verb runs the command it names, in the controller's own binary, and answers what the command
|
||||||
|
printed** — the output, whether it succeeded, and, where the command speaks JSON, the same as data. A
|
||||||
|
refusal is the command's refusal in the command's words, because it is the same output. A verb takes
|
||||||
|
only the arguments its schema names; nothing reaches a flag the schema did not declare. `push` and
|
||||||
|
`build` are sent and not waited for: a call that blocked for a whole apply would time out on every
|
||||||
|
machine that takes a minute and say nothing about the others.
|
||||||
|
|
||||||
|
**The three prerequisites are built.** A seat's protocol is three columns on its row, seeded from the
|
||||||
|
compiled defaults where a row had none and additively thereafter, so a verb a release adds joins the
|
||||||
|
row and nothing an operator wrote is taken away. A served verb is its name, what it does, and the
|
||||||
|
schema of its arguments and answer; a manifest may still write a bare name. A node-scoped seat's tool
|
||||||
|
carries the node it is asked of, as the last token of its subject; a mesh-scoped seat's stays flat.
|
||||||
|
|
||||||
|
**Holding a mesh seat requires serving its verbs**, judged where the store's set is loaded, and the
|
||||||
|
refusal names the missing verbs. The controller's own manifest lists the twelve under `tools`.
|
||||||
|
|
||||||
|
**Discovery reads the records, through the seat.** `tools` is one of the twelve because the console
|
||||||
|
cannot read the store and should not: the mesh answers for its own records through the role that owns
|
||||||
|
them, and the answer is true while any *other* holder restarts. It is not true while the control plane
|
||||||
|
itself restarts, and the console says so rather than hiding the modules' tools with it.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
> **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
|
||||||
|
|
||||||
|
- **The console answers the mesh's own questions.** Issue 147's first paragraph closes: what a node
|
||||||
|
runs, what is assigned, whether a push applied, from the machine the person sits at, over the bus,
|
||||||
|
under an account whose permission list says so.
|
||||||
|
- **Whoever may call `mesh-controller.push` may change the mesh.** That is the console's `*` on a
|
||||||
|
machine whose login owns the mesh (0152), and a person's account only if `operator issue` says so.
|
||||||
|
A grant reviewer reads `*` and `seat:mesh-controller.` with the same care.
|
||||||
|
- **A verb here binds every future controller.** Twelve is deliberate: what an operator asks weekly,
|
||||||
|
and nothing that is still finding its shape (`take`, `converge`, `settings`, `secret` stay commands).
|
||||||
|
- **A command's text is the answer**, and text changes. The three verbs that speak JSON carry it as
|
||||||
|
data; the rest are read by a person or an agent, not parsed. Anything that needs a shape asks for
|
||||||
|
`--json` to be added to the command first, which is the right order.
|
||||||
|
- **What got harder:** the mesh-controller seat's row now carries a protocol an operator could edit, and
|
||||||
|
a verb removed from the row is a verb the controller stops serving without a build. That is
|
||||||
|
ADR 0122's arrangement applied to tools, and `seats` shows the row.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| Every declared verb is one the binary can run, with the arguments its schema names | a test walks the table and derives a command line for each |
|
||||||
|
| A verb missing a required argument is refused in its own words, before anything runs | a test per shape |
|
||||||
|
| A holder that does not serve a mesh seat's verbs cannot hold it, and the refusal names them | a catalogue test against a seat with two verbs and a holder with one |
|
||||||
|
| A node-scoped seat's tool carries the node; a mesh seat's does not | the bus composition test: two nodes derive two addresses |
|
||||||
|
| The controller subscribes its seat's tools and may answer | the composition test, and the golden user list |
|
||||||
|
| `*` reaches a role's tools; `seat:` grants one and refuses a name with no verb | the composition test |
|
||||||
|
| The protocol is seeded into the row and widened additively | the store-backed seat test |
|
||||||
|
| Live: the console lists `mesh-controller.status` and a call answers what `status --json` prints | the rollout of this record |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — extended: the prerequisites built, the verbs decided
|
||||||
|
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — the surface that lists them
|
||||||
|
- [ADR 0035](0035-one-implementation-several-surfaces.md) — a surface is an adapter with no decisions in it
|
||||||
|
- [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md) — the row is the mesh's, and now carries the protocol
|
||||||
|
- [33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) — the design this completes
|
||||||
@@ -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)
|
||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-02, by [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md).** Everything decided here stands: one runtime per node, host-side, every module's tools and every held seat's verbs on the memberships' subjects, root the module's concern, any node calls any tool, the console its serving mode. What moved is how the runtime brings a bundle to life. Decision 3 and the consequence *the node tools runtime needs an interpreter on the machine* read as though a bundle were always interpreted code the runtime imports; a tools bundle is now a process in any language that speaks MCP over stdio to the runtime, and importing a TypeScript bundle is the shortcut, not the contract.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
A module's tools are code the module wrote, one function behind each verb, served on the subjects
|
||||||
|
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)
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 179. The intrusion seat serves its verbs, a container may log to the journal, and every door declares its jail
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Read on the control node on 2026-10-02, the day the machines were confirmed filtered by the mesh
|
||||||
|
alone ([ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)): the intrusion
|
||||||
|
prevention watched one door. Its two jails read the ssh daemon's journal and its own log, banned
|
||||||
|
five failures in ten minutes for ten minutes, and in a day had seen twelve thousand failed logins
|
||||||
|
from three hundred addresses and banned none of the busiest, which paced themselves at one try every
|
||||||
|
ten minutes. The mail submission port took a hundred and sixty password guesses in the same day from
|
||||||
|
thirty-eight addresses with no jail reading it at all; the forge and the public proxy had no jail
|
||||||
|
either, and the proxy logged nothing a jail could read. Nobody could see the jails without a shell:
|
||||||
|
the module's three tools existed in code and were served by nothing, and the seat it holds declared
|
||||||
|
no verbs.
|
||||||
|
|
||||||
|
Three things were missing and they are three shapes the mesh already has. The packet filter's seat
|
||||||
|
serves verbs every holder owes ([ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)); the
|
||||||
|
intrusion seat serves none. A module's `listens` compose into the machine's filter, and [to-be 31](../03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md)
|
||||||
|
says a module's `jails` compose into the machine's intrusion prevention the same way — the controller
|
||||||
|
composes them, and no module declares one. And a jail reads a log; a container's output goes to a
|
||||||
|
file of the runtime's own under a path that changes when the container is recreated, which is why
|
||||||
|
no jail could read the mail front end, the forge or the proxy, however they logged.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. The `node-intrusion-prevention` seat serves four verbs**, and a module that claims it serves
|
||||||
|
all four or is refused the claim, as with every seat:
|
||||||
|
|
||||||
|
- `status` — every jail with what it watches, how many addresses it is counting failures against and
|
||||||
|
holding now, and the totals since it started; one jail's detail when named. Read-only.
|
||||||
|
- `banned` — every address banned now, with the jail holding it, when it was banned and when the ban
|
||||||
|
ends. Read-only.
|
||||||
|
- `ban` — ban one address in one jail now, for that jail's ban time. An operator's act on the live
|
||||||
|
ban list, which the mesh composes the rules for and never writes itself.
|
||||||
|
- `unban` — let one address go, from one jail or from every jail.
|
||||||
|
|
||||||
|
A holder may serve its own tools beside these; the fail2ban module reads one jail's effective
|
||||||
|
settings as its own.
|
||||||
|
|
||||||
|
**2. A container may log to the journal.** `logging: journald` on a container has the host run it
|
||||||
|
with the journal as its log driver; the journal keeps the container's name on every line, and
|
||||||
|
`docker logs` keeps working. Where a container logs is part of its spec, so moving it recreates the
|
||||||
|
container, and the only place besides the runtime's own file is the journal: a machine's intrusion
|
||||||
|
prevention reads the journal already, for the ssh daemon, and a container that logs there is read
|
||||||
|
the same way, by the container's name, whatever the container is called by the runtime this time.
|
||||||
|
|
||||||
|
**3. A module with a door declares its jail, and the holder composes them.** What to-be 31 designed
|
||||||
|
is now the rule: a module whose service authenticates from outside — the mail front end, the forge,
|
||||||
|
the public proxy — declares in its manifest what a failed attempt looks like in its log and how to
|
||||||
|
ban on it, naming no node and no path; the module that holds the intrusion seat declares where the
|
||||||
|
composed jails and filters land, and the mesh writes them on every machine that runs both. A machine
|
||||||
|
not running the module has no such jail. The holder restarts its daemon on the composed file.
|
||||||
|
|
||||||
|
**4. The base is strict, and the mesh's own range is never banned.** Three failures in a day ban for
|
||||||
|
a day, on every jail unless the jail says otherwise; banned twice in two weeks, by any jail, is
|
||||||
|
banned for four. The attackers this mesh sees pace themselves under any ten-minute window; a day's
|
||||||
|
window counts them. A person who mistypes three times from one address is out for a day from that
|
||||||
|
address, and never from a machine of the mesh, whose range stays in the never-banned list the module
|
||||||
|
has carried since [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md). The operator
|
||||||
|
chose this knowing it.
|
||||||
|
|
||||||
|
**5. The proxy says a refused name in its log.** A request for a name this mesh does not serve, from
|
||||||
|
outside, is what a scanner does; the proxy already logged a certificate refused for such a name, and
|
||||||
|
now logs the plain request too, with the asking address last, as its own jail's filter expects it.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The seat's row gains four verbs; a mesh that already runs widens its row at the next controller
|
||||||
|
start. The fail2ban module claims them and gains a runtime — a tool server whose image carries the
|
||||||
|
fail2ban client, with the daemon's socket shared in from the machine, and nothing else of the
|
||||||
|
machine. The daemon stays the machine's; what runs in the container is only the client.
|
||||||
|
- **That runtime is the shape the catalogue has today, and it is on its way out.**
|
||||||
|
[ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md),
|
||||||
|
accepted the same day as this record, replaces a tool container per module with one tool runtime
|
||||||
|
per node on the host side, taking each module's tools as a bundle. Nothing here depends on the
|
||||||
|
container: the verbs, the client that speaks to the daemon over its socket, and the jails are the
|
||||||
|
same code under either. This module converts with the packet filter's, whose runtime that record
|
||||||
|
names, and the socket it needs becomes the node runtime's to reach rather than a mount of its own.
|
||||||
|
- The host's container vocabulary grows by `logging`; an older host refuses a declaration that carries
|
||||||
|
it, so the host rolls before the modules. Three containers are recreated once, when their modules
|
||||||
|
are pushed with the field: the mail front end, the forge and the proxy — each a moment's outage.
|
||||||
|
- The fail2ban module declares where jails compose (`jailing`) and the directory the filters go in;
|
||||||
|
the mail, forge and proxy modules each declare one jail reading the journal by their container's
|
||||||
|
name. The composed jail file is the one resource the daemon restarts on when a module arrives or
|
||||||
|
leaves a machine.
|
||||||
|
- The two base jails and the composed ones take the day's window; the ssh jail's ten minutes are
|
||||||
|
gone. An address banned on the first day of this record stays banned for the day.
|
||||||
|
- The module's three old tools, served by nothing, are replaced by the seat's four verbs and one
|
||||||
|
own tool; `fail2ban_status` as a name is gone.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| The seat declares the four verbs; a claim that serves fewer is refused by name | the catalogue's seat tests |
|
||||||
|
| `status`, `banned`, `ban` and `unban` read and steer the daemon through its client, with the shapes fail2ban 1.1.0 printed live; a non-address and a non-name are refused before anything runs | the module's tests over a fake command runner |
|
||||||
|
| A container's `logging` reaches the runtime's arguments and its spec; a place other than the journal is refused | host tests |
|
||||||
|
| A module's jails compose into the holder's file and a filter per jail, and the file is written empty when none is declared | the controller's composition tests (to-be 31) |
|
||||||
|
| The proxy logs a refused name with the address last | the proxy's tests |
|
||||||
|
| A jail's pattern names `<HOST>` once per shape, since two is a duplicate capture group and costs the machine every ban | the catalogue's manifest tests |
|
||||||
|
| Live | done 2026-10-02: `status` and `banned` answered on both servers through the console; the proxy's jail counted seven refusals on the home server; a documentation address banned in the ssh jail came back with its end time and was released |
|
||||||
|
|
||||||
|
## Built and proven live, 2026-10-02
|
||||||
|
|
||||||
|
All five rules are in the mesh. The host carries `logging`; the controller's seat row carries the four
|
||||||
|
verbs and the proxy says a refused name in its log; the fail2ban module holds the seat from a runtime
|
||||||
|
with the daemon's socket shared in, composes the jails, and the mail front end, the forge and the
|
||||||
|
proxy each declare one. Through the console on the control node: `status` listed five jails with what
|
||||||
|
each watches, `banned` listed the nine the long jail holds, and a documentation address banned in the
|
||||||
|
ssh jail came back with its ban's end time and was released again. On the home server the proxy's jail
|
||||||
|
had counted seven refusals within minutes of starting.
|
||||||
|
|
||||||
|
**One fault, found by the machine and not by a test.** The proxy's pattern matched two shapes of
|
||||||
|
refusal in one expression and so named `<HOST>` twice. fail2ban expands that placeholder into a named
|
||||||
|
capture group; two of them is a duplicate group name, and the daemon refuses *its whole configuration*
|
||||||
|
and exits — both servers kept no bans at all for about ten minutes, every jail and not the one at
|
||||||
|
fault. The pattern is now one per shape. A manifest check refuses the mistake at merge time, naming
|
||||||
|
what it would cost, which is the only reason this record can claim the rule rather than the instance.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md), [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md), [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)
|
||||||
|
- [Design 31 — A module declares its fail2ban jail](../03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.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,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 | done 2026-10-02: both machines report ufw gone — `pacman -Q ufw` has no answer, `node show` says *removed*, `status` is well. The home server said *retired* for six hours after the package went, because this record's step runs only after a clean apply ([ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)) and one dead tracker was failing its applies ([ADR 0187](0187-a-dead-tracker-is-not-the-machines-failure.md)) |
|
||||||
|
|
||||||
|
## 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)
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-27
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: true
|
||||||
|
extends: 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 181. The operator account is a node fact, and a home is a placement root
|
||||||
|
|
||||||
|
*Reconstructed. The controller shipped this on 2026-09-27 and
|
||||||
|
[to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) recorded it as built without a
|
||||||
|
decision behind it. This record states what was decided, from the code and the design, and adds the
|
||||||
|
two rules the code left implicit — what an empty account means for a module, and that the account is
|
||||||
|
stated rather than discovered. Written 2026-10-02.*
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) took every host path out of a
|
||||||
|
module definition and gave a module's *system* data a place: a directory the mesh resolves under the
|
||||||
|
node's root, owned by the module. It said nothing about the other half of a filesystem — the files
|
||||||
|
that belong under a person's home and are owned by that person. The predecessor wrote several of
|
||||||
|
those: the ssh client configuration, the shell's configuration, an agent's instruction files. It knew
|
||||||
|
whose home it was writing into because each of its node records carried a login name. The mesh took
|
||||||
|
the machine facts over and dropped the human one.
|
||||||
|
|
||||||
|
The loss was found the ordinary way: `ssh <node>` logged into the home-server under the workstation's
|
||||||
|
own login name, because nothing in the mesh said the home-server's account was a different one
|
||||||
|
([to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md),
|
||||||
|
[issue 172](../04-ISSUES/172-the-ssh-client-block-matches-one-spelling-of-a-machine/00-report.md)).
|
||||||
|
|
||||||
|
What the controller does since 2026-09-27: a node record carries an operator account and, optionally,
|
||||||
|
its home; the account and its home are machine facts a definition may name in a resource's path, owner
|
||||||
|
and content; a roster file may say it lives under the home, and is then rendered per node, placed under
|
||||||
|
that node's account's home, owned by the account, and left out on a node with no account. On
|
||||||
|
2026-10-02 **all four nodes of the live mesh carry an empty account**: the fact exists and nobody has
|
||||||
|
stated it, so no home-scoped resource can land anywhere yet.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **The definition names the login.** `owner: <name>` in the module. Rejected: it is the installation
|
||||||
|
written into a definition, which ADR 0112 forbids and
|
||||||
|
[ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md) checks for, and it is
|
||||||
|
wrong on the first machine whose login differs — which is exactly the machine that surfaced this.
|
||||||
|
2. **The host discovers the account.** The first non-system user, or whoever ran the enrolment.
|
||||||
|
Rejected: a guess. A shared machine has several people on it, a server may have none, and a host
|
||||||
|
deciding whose files these are is a decision the mesh then cannot see, state or correct.
|
||||||
|
3. **The account is a fact the operator states on the node record, and the home is derived from it
|
||||||
|
unless stated.** Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**A node has an operator account: the login name of the person who works on it.** It is stated by the
|
||||||
|
operator on the node record, the way a node's address or mode is held there, and it is empty for a
|
||||||
|
machine nobody logs into. Empty is a real state, not a missing value. The mesh holds the fact because
|
||||||
|
everything below derives from it, and because it is precisely the fact that was lost when the
|
||||||
|
predecessor's records were not carried over.
|
||||||
|
|
||||||
|
**The account's home is derived unless stated.** The superuser's home for the superuser, the
|
||||||
|
distribution's conventional per-user home otherwise; a node whose account lives elsewhere states its
|
||||||
|
home. One place computes the default, so a fact and the record cannot disagree about it.
|
||||||
|
|
||||||
|
**A resource may be placed under the home, owned by the account.** This is ADR 0112's move one level
|
||||||
|
over: as a module's system directory is resolved under the node's root, a file under a person's home is
|
||||||
|
resolved against the account's home, and owned by the account rather than by root or a module's own
|
||||||
|
account. A definition names the account and its home as machine facts, never as a path; a roster fact
|
||||||
|
may say it is a home file and is then placed and owned the same way. The controller resolves both at
|
||||||
|
composition, and the host chowns what it creates.
|
||||||
|
|
||||||
|
**A node with no account cannot carry a home-scoped resource, and says so.** A roster fact that lives
|
||||||
|
under the home is left out of that node's declaration rather than written to nowhere. A resource naming
|
||||||
|
the account fact on such a node is refused at composition, naming the fact the machine does not have.
|
||||||
|
A module that writes a person's files is thereby unassignable to a machine with no person on it, which
|
||||||
|
is the right refusal.
|
||||||
|
|
||||||
|
**One account per node is what this record decides.** Several people on one machine is left open, with
|
||||||
|
the constraint that allowing it must not force the common case — one workstation, one person — to name
|
||||||
|
anything.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **The operator states the account before any home-scoped module lands.** Today none is stated, so the
|
||||||
|
first assignment of such a module begins with four node records.
|
||||||
|
- The roster carries each node's account, so a composed ssh configuration logs in as the right person
|
||||||
|
on every machine — the gap that surfaced this, closed by the same fact.
|
||||||
|
- A family of modules becomes writable: everything the predecessor placed under a home — ssh client,
|
||||||
|
shell, the agent's instruction files — is now a module naming a fact rather than a path
|
||||||
|
([to-be 29 §2](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md)).
|
||||||
|
- **What got harder:** a definition cannot say "my user's home" without the mesh knowing who the user
|
||||||
|
is, so a module of this family is refused on a freshly enrolled machine until a person is named on
|
||||||
|
it. That is a prompt, not an obstacle.
|
||||||
|
- **Not decided here:** several accounts per node; a service unit running as the account rather than
|
||||||
|
as root or a module; a one-off step run as the account. Each is a record of its own.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A resource's path and owner resolve the account and its home | controller tests on machine-fact resolution: a file naming the account facts lands under the account's home, owned by the account |
|
||||||
|
| The home is derived unless stated | a controller test: the superuser's home for the superuser, the conventional home otherwise, the stated home when one is stored |
|
||||||
|
| A home roster fact is left out on a node with no account | a controller test on roster composition: the file is absent from that node's declaration and present on a node with an account |
|
||||||
|
| A resource naming the account on a node with no account is refused by name | a controller test on machine-fact resolution: the refusal names `account` and lists the facts the machine does have |
|
||||||
|
| No definition names a home path | ADR 0112's catalogue test on host paths, which a `/home` or `/root` literal fails |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) — the design this record
|
||||||
|
gives a foundation to, and its "what has shipped" section
|
||||||
|
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) — the system-path placement
|
||||||
|
this mirrors; [ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md) — why
|
||||||
|
a login name may not be in a definition
|
||||||
|
- [ADR 0120](0120-a-roster-fact-carries-its-format-as-a-template.md) — the roster fact a home file
|
||||||
|
may be
|
||||||
|
- [ADR 0182](0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md) — what
|
||||||
|
the mesh may and may not do inside the home this record lets it reach
|
||||||
|
- mesh-controller `internal/inventory/nodes.go` (the account and its home on the node record),
|
||||||
|
`internal/catalogue/machine_into_files.go` and `roster.go` (resolution and the home fact)
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 182. Inside a home, the mesh owns the directory and the files it places, writes into the tool's own files, and holds everything else as found
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0181](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md) lets a module
|
||||||
|
place files under a person's home. A home is unlike any directory the mesh has written into so far:
|
||||||
|
it is shared with the person, and with every program the person runs. The agent's configuration
|
||||||
|
directory on the laptop makes the point. On 2026-10-02 it holds thirty entries. The predecessor placed
|
||||||
|
five of them (an instruction file, a conventions rule, a settings file it merged into, two skills); a
|
||||||
|
sibling module placed a sixth (the node's identity rule). The agent itself writes the other
|
||||||
|
twenty-four: its settings, its credentials, its history, the memory of every project it has worked in,
|
||||||
|
its plugins, its session logs. Several of those are what [to-be 15](../03-DESIGN/01-to-be/15-the-agent-session.md)
|
||||||
|
calls memory *written by the session itself and declared by nobody*: a mechanism that regenerated the
|
||||||
|
directory would erase a season of it, silently, while reporting success.
|
||||||
|
|
||||||
|
The predecessor's own module recorded the hazard in the other direction. Its settings file was first
|
||||||
|
shipped as *replace*, and every `/model` choice a person made inside a session was reverted to the
|
||||||
|
template's value on the next synchronisation — on every node, indefinitely, with no indication why. It
|
||||||
|
was changed to *merge*, and the comment explaining why is still in its manifest.
|
||||||
|
|
||||||
|
**And the generator is gone while its output stayed.** The predecessor was retired from the laptop on
|
||||||
|
2026-10-01. Its six files are still on both workstations, with their content telling every session to
|
||||||
|
use tools that no longer exist. Nothing owns them; nothing will ever rewrite or remove them.
|
||||||
|
|
||||||
|
[To-be 29 §3](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) drew the line for one directory,
|
||||||
|
`~/.ssh`: the mesh owns the directory and the files it places; it holds the person's private keys and
|
||||||
|
personal drop-ins as found. That was argued from the lockout `~/.ssh` can cause. The argument here is
|
||||||
|
the same shape with a different stake — the person's work rather than the person's way in — and it has
|
||||||
|
to hold for every directory the family of home-scoped modules will touch, so it is a rule, not a
|
||||||
|
section.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **The module owns the directory whole**, regenerating it from the definition. Rejected: it destroys
|
||||||
|
the memory, history and local settings the agent writes for itself, which is the failure to-be 15
|
||||||
|
names and the predecessor's settings file demonstrated at small scale.
|
||||||
|
2. **The module owns only the files it names, and nothing about the directory.** Rejected: *owning one
|
||||||
|
file beside foreign ones is not owning anything* (to-be 29). The directory must exist, with the right
|
||||||
|
owner and mode, before the tool first runs on a fresh machine; and a credentials file in a
|
||||||
|
world-readable directory is a credentials file in the wrong directory.
|
||||||
|
3. **The module owns the directory and the files it places; a file the tool writes for itself is
|
||||||
|
written into, never over; everything else is held as found.** Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**A home-scoped module owns the directory it declares: its existence, owner and mode.** The host creates
|
||||||
|
it if absent, owned by the account, and never removes it while it holds anything
|
||||||
|
([ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)). Inside it, every path the module touches
|
||||||
|
is in exactly one of four classes, and **the class is visible in the definition from the shape
|
||||||
|
declared**, not inferred from what happened to be on disk:
|
||||||
|
|
||||||
|
| class | declared as | the host's rule |
|
||||||
|
|---|---|---|
|
||||||
|
| **owned** | a file with content, or a roster fact | written whole, regenerated, removed when undeclared; a file found there with no record of the mesh making it is kept once before it is written over ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)) |
|
||||||
|
| **written into** | a file written *into* a structured document | only the keys the definition names are set, every other key is kept, and each set key is given back when undeclared (ADR 0102). The key list is the module's and is short |
|
||||||
|
| **written by the module's own process** | nothing the host applies: the module's code writes it from what it was handed | the file's content is never a declared file's content, because a declaration travels in the clear on the bus and the host records it; the module's code writes it, owned by the account, atomically. [ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) says how for a credential |
|
||||||
|
| **found** | nothing | never read, never rewritten, never removed. The person's memory, history, projects, local settings, their own rules and skills |
|
||||||
|
|
||||||
|
**A file the tool writes for itself is written into, never over.** The agent's settings file and its
|
||||||
|
own state file are the tool's; the mesh has one or two facts to state in each. Setting those keys and
|
||||||
|
nothing else is what lets a person's `/model` choice survive a push, and what lets the mesh's keys be
|
||||||
|
taken back cleanly when the module goes.
|
||||||
|
|
||||||
|
**A predecessor's output is found.** A file placed by a generator that no longer exists is, to the
|
||||||
|
mesh, a file it has no record of making. Where the successor module keeps the path, declaring it
|
||||||
|
*adopts* it: the host keeps the original once and writes the mesh's. Where the successor does not keep
|
||||||
|
the path, the mesh does not remove the file, because it removes nothing it did not make; **the operator
|
||||||
|
removes it, once**, and the module's definition names those paths in its own documentation so the step
|
||||||
|
is not forgotten. This is the first instance of the one-off setup step to-be 29 leaves open, and the
|
||||||
|
rule chosen for it is that it is a person's act, listed, not a module's.
|
||||||
|
|
||||||
|
**The rule is the family's.** An ssh client module, a shell module, an agent module each declare their
|
||||||
|
directory and classify their paths this way. A module that cannot say which class a path is in has not
|
||||||
|
finished its definition.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- A person's work under their home survives every push and every unassign. The mesh's own files come
|
||||||
|
and go with the module; the mesh's keys in the tool's files come and go with it; the directory stays.
|
||||||
|
- **Stale files survive too.** Two workstations keep three predecessor files each until a person removes
|
||||||
|
them — a visible cost, accepted over a mesh that deletes under a person's home. A module author who
|
||||||
|
renames one of the mesh's own files has the ordinary path: the old resource id is undeclared and the
|
||||||
|
host removes what it made ([ADR 0118](0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md)).
|
||||||
|
- A module's definition is longer by a classification, and a reviewer has one more question per path.
|
||||||
|
That is the point: *which parts are managed must be explicit rather than inferred* (to-be 15).
|
||||||
|
- **What got harder:** a module cannot seed a person's preference once and leave it. A seeded file
|
||||||
|
([ADR 0087](0087-a-seeded-file-is-created-once.md)) is the shape for that, and it is available to
|
||||||
|
this family unchanged; what is refused is a seed the module later wants to change, because what grew
|
||||||
|
in it is the person's.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| The directory is created owned by the account and kept when the module goes | host tests of a directory resource with an owner (ADR 0051's and 0118's), and the family's lab check below |
|
||||||
|
| An owned file found with no record is kept once, then written | host tests of ADR 0102's kept-original rule |
|
||||||
|
| Only the declared keys of a written-into file change, and are given back | host tests of ADR 0102: declared keys set, the rest kept, restored when undeclared |
|
||||||
|
| Nothing found is touched | the family's lab check: a machine with a seeded home holding a person's file beside a predecessor's; after apply the person's file is byte-identical, the predecessor's is kept as the original, the mesh's keys are set and the person's keys in the same file remain; after unassign the mesh's files are gone, the keys are restored, the person's files are untouched and the directory stands |
|
||||||
|
| Every path a home-scoped module touches is classified | a catalogue review rule for this family: each path is a directory, a file, a file written into, a secret-and-step, or absent — the first module written to it is [to-be 36](../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md) |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [to-be 29 §3](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) — the same boundary drawn for `~/.ssh`
|
||||||
|
- [to-be 15](../03-DESIGN/01-to-be/15-the-agent-session.md) — a session's memory is declared by nobody
|
||||||
|
- [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), [ADR 0087](0087-a-seeded-file-is-created-once.md),
|
||||||
|
[ADR 0118](0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md), [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md) — the mechanics each class rests on
|
||||||
|
- [ADR 0051](0051-shared-data-is-the-operators.md) — the third case the host had no word for: what it neither made nor configured
|
||||||
|
- the predecessor's `claude-code` module manifest, whose comment on `strategy: merge` records the reverted `/model` choice
|
||||||
+163
@@ -0,0 +1,163 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0024-model-access-is-a-provision.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 183. The Anthropic licence manager is a module holding a seat; it hands each node's agent its token over the bus, sealed; the controller and the host have no part
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
**The operator's stance, set on 2026-10-02 and sharpened during the day.** The controller has no part in
|
||||||
|
the agent module. The host is module-agnostic: it knows no vendor, no agent, no path under a home. The
|
||||||
|
agent module owns its own files. And there must be a *real* licence manager — a module that doles out
|
||||||
|
the correct licence in every situation the mesh has: two subscription accounts and one API key today,
|
||||||
|
used by a person's interactive agent on each workstation, by the mesh's own sessions, and by workers.
|
||||||
|
|
||||||
|
**What the predecessor built, read from its code the same day.** Two modules, split after an incident.
|
||||||
|
A *manager* on exactly one node held every account's full OAuth grant encrypted, rotated each grant
|
||||||
|
under a per-licence lease on a cadence and an expiry floor, published each rotation over its bus with
|
||||||
|
the tokens encrypted, collected the vendor's usage figures per licence, and alerted once a day on
|
||||||
|
repeated failure or on a refresh token within three days of its own expiry. A *consumer* on every node
|
||||||
|
was the single writer of the agent's credentials file: it applied a published rotation, stripped the
|
||||||
|
refresh token so a node could never rotate, pulled when stale, refused a stale grant by comparing
|
||||||
|
expiries within one lineage, and mirrored a local login back to the manager only after checking the
|
||||||
|
account's identity against the licence's record — because an unchecked mirror had once written one
|
||||||
|
account's grant into another's row and published it mesh-wide. Three **touchpoints** with fallbacks: the
|
||||||
|
node's interactive agent; the mesh's own sessions on the node, falling back to the node's licence; a
|
||||||
|
worker's own account, falling back to the node's, and refusing to spawn when assigned a licence that
|
||||||
|
could not be served. The split exists because four nodes refreshing one grant destroyed it: an OAuth
|
||||||
|
refresh rotates the refresh token, and the predecessor's own code records both that a reused token
|
||||||
|
killed a licence and that a malformed client id was once misdiagnosed as the same fault. **Whether a
|
||||||
|
refresh token is single-use is not documented by the vendor**; the predecessor treated it as so, and
|
||||||
|
this record keeps one rotation source for that reason while leaving the fact to be measured.
|
||||||
|
|
||||||
|
**What the mesh has.** [ADR 0050](0050-model-access-is-vendor-agnostic.md) put a per-vendor adapter
|
||||||
|
inside the controller's licences context, with the carve-out that the manager node holds the refresh
|
||||||
|
token readably; the catalogue has a manager and a consumer module built on it, assigned to nothing. The
|
||||||
|
controller's licence commands are not seat verbs and cannot be asked for through the console
|
||||||
|
([to-be 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)). `model-access` is a vendor-blind
|
||||||
|
provision ([ADR 0024](0024-model-access-is-a-provision.md)), and the operator's judgement is that the
|
||||||
|
agent is not a vendor-blind consumer: it is coupled to an Anthropic subscription grant and nothing else,
|
||||||
|
so a name that hides the vendor misdescribes the coupling
|
||||||
|
([ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md)).
|
||||||
|
|
||||||
|
**The bus's rule for a secret** ([to-be 32 §10](../03-DESIGN/01-to-be/32-what-a-module-declares.md)): the
|
||||||
|
bus is not trusted with one; a secret travels sealed to its recipient, on core request/reply, never
|
||||||
|
through a stream that persists it.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep the lifecycle in the controller** ([ADR 0050](0050-model-access-is-vendor-agnostic.md) as
|
||||||
|
built), and make the agent module a consumer of `model-access` delivered by the host as a sealed
|
||||||
|
file. Rejected by the operator: the controller and the host would both carry a part of an
|
||||||
|
Anthropic-specific mechanism, and the agent's coupling is misnamed.
|
||||||
|
2. **The manager delivers each short-lived token through the vault**, as a backend-issued secret the
|
||||||
|
vault provides to each consumer ([ADR 0113](0113-the-vault-makes-every-secret.md)). Rejected: every
|
||||||
|
hourly rotation becomes a vault delivery, a composition and a push to every node, and the host
|
||||||
|
ends up writing a vendor's credential as a file — the module-agnostic host, carrying a vendor's
|
||||||
|
traffic.
|
||||||
|
3. **A seat-holding manager module that talks to the agent module on every node over the bus.**
|
||||||
|
Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**The Anthropic licence manager is a module, `claude-licence-manager`, holding the mesh-scoped seat
|
||||||
|
`anthropic-licence-manager`.** The seat's contract is the licence verbs: list the licences and their
|
||||||
|
health, list the bindings, bind or switch a consumer, release one, refresh now, read usage, adopt a
|
||||||
|
grant, register a node's key, answer a consumer's current token. One holder, on a node the operator
|
||||||
|
assigns, is what makes rotation happen once ([ADR 0126](0126-a-module-declares-its-own-seats.md),
|
||||||
|
[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)). The seat is named for the vendor,
|
||||||
|
because what it manages is one vendor's grants and nothing else is coupled to it. The vendor-blind
|
||||||
|
`model-access` provision stands for the consumers that do not care which vendor answers; the agent is
|
||||||
|
not among them.
|
||||||
|
|
||||||
|
**The manager owns the licences.** The records, the grants, the bindings per touchpoint, the usage
|
||||||
|
readings and the audit of every switch live in the manager's own store, not in the controller's
|
||||||
|
licences context, which keeps only what it already serves to vendor-blind consumers. The manager is the
|
||||||
|
one rotation source: it alone calls the vendor's token endpoint, under a lease per licence, on an expiry
|
||||||
|
floor and a cadence it declares as a setting.
|
||||||
|
|
||||||
|
**The long-lived grants are encrypted at rest with a key the vault made for the manager.** The vault
|
||||||
|
keeps custody of that one key as the manager's own secret ([ADR 0113](0113-the-vault-makes-every-secret.md));
|
||||||
|
the grants themselves — a refresh token per subscription account, the API key — are the manager's
|
||||||
|
rows, readable only by it. This is [ADR 0050](0050-model-access-is-vendor-agnostic.md)'s carve-out,
|
||||||
|
moved with the manager: *one module, one node, the long-lived grants only.*
|
||||||
|
|
||||||
|
**The short-lived tokens travel module to module, sealed, on request/reply.** The agent module on each
|
||||||
|
node makes a keypair of its own when it first runs — a private key made where it is used, never leaving
|
||||||
|
([ADR 0113](0113-the-vault-makes-every-secret.md)) — and registers its public half with the seat. The
|
||||||
|
manager hands a node its token by calling that node's agent module (`<module>.<tool>@<node>`,
|
||||||
|
[ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)) with the token
|
||||||
|
sealed to that key, and the module answers *applied* or *refused* and why. An agent module that starts,
|
||||||
|
or finds its token near expiry, asks the seat for its current token the same way. **A token is never
|
||||||
|
published as an event**: what the manager emits — rotated, switched, failing, usage read — names the
|
||||||
|
licence and nothing secret, and the audit logger records it. This is a second channel for a secret
|
||||||
|
beside the vault's, and it is bounded as 0050's carve-out is: this vendor, tokens that live hours, sealed
|
||||||
|
to one recipient, request/reply only.
|
||||||
|
|
||||||
|
**The agent module alone writes what the agent reads.** For a subscription licence it writes the
|
||||||
|
agent's credentials file under the operator's home, as the operator, access-token-only, atomically. For
|
||||||
|
the API-key licence it serves the key through the agent's own key-helper setting, so nothing is written
|
||||||
|
under the home at all. For the mesh's own sessions and workers on that node, it is the local source of
|
||||||
|
their token. **The host delivers the module's package and its state directory and knows nothing else**:
|
||||||
|
no path under the home, no vendor, no file shape.
|
||||||
|
|
||||||
|
**A binding is explicit, and a switch is a reaction.** Every consumer — a node's interactive agent, the
|
||||||
|
mesh's session on a node, a worker — is bound to a licence by the operator through the seat's verb, with
|
||||||
|
the predecessor's fallbacks: a session inherits its node's licence, a worker inherits its node's, and a
|
||||||
|
worker assigned a licence that cannot be served is refused rather than lent another. Exhaustion is
|
||||||
|
observed and warned about once per crossing of a declared threshold; moving a consumer to another
|
||||||
|
licence is a person's act through the seat's verb, as [ADR 0024](0024-model-access-is-a-provision.md)
|
||||||
|
says, and the declaration language grows no conditional. An automated policy is not decided here.
|
||||||
|
|
||||||
|
**A login is attributed only to the account it belongs to.** When a person logs in on a node, the
|
||||||
|
agent module reads the account's identity from the agent's own state and offers the grant to the
|
||||||
|
manager sealed to the manager's key; the manager adopts it only when the identity matches the licence
|
||||||
|
the node is bound to, and refuses with a notification otherwise.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- One module decides which licence every consumer gets, one module writes what each agent reads, and
|
||||||
|
neither the controller nor the host carries a word of the vendor.
|
||||||
|
- **A second sealed channel exists** beside the vault's, bounded as stated. A record that widens it to
|
||||||
|
another vendor or a longer-lived secret is a new decision, not an application of this one.
|
||||||
|
- The catalogue's `anthropic-manager` and `anthropic-consumer` modules, built on ADR 0050's placement,
|
||||||
|
are retired once the manager runs; the controller's licences context stops holding Anthropic licences.
|
||||||
|
- The console lists the seat's verbs, so a person switches a licence in a sentence, and the controller
|
||||||
|
gains no `licence` verb.
|
||||||
|
- **What got harder:** a manager that is down leaves every node on its last token until it expires;
|
||||||
|
the agent module keeps the last token and says so. And a node whose agent module has not registered
|
||||||
|
its key cannot be handed a token, which the manager reports by name.
|
||||||
|
- Every interactive session on a machine shares the node's one agent directory, and so its licence;
|
||||||
|
twenty sessions share it as one does. A consumer with a licence of its own on the same machine is a
|
||||||
|
worker running from a home of its own with its own agent directory — the worker touchpoint above, for
|
||||||
|
when workers exist ([ADR 0003](0003-agents-are-persistent-employees.md)); the predecessor ran its
|
||||||
|
agents that way.
|
||||||
|
- **Not decided here:** an automated switch on exhaustion; whether a refresh token is single-use, to be
|
||||||
|
measured in the lab.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| Only the seat's holder calls the vendor's token endpoint | a catalogue test: no module but the manager names it; the manager's refresh runs under a lease per licence, tested with two concurrent runs |
|
||||||
|
| A token crosses the bus only sealed, only on request/reply | a bus test: every message the manager publishes as an event carries no token; the hand-over is a request whose payload opens only with the receiving module's key |
|
||||||
|
| The agent module's private key never leaves the node | the per-key test of ADR 0113, extended to this module's key |
|
||||||
|
| The host writes nothing under a home and names no vendor | a catalogue test on the agent module's definition: no file resource under a home, no vendor word in anything the host applies |
|
||||||
|
| A grant is attributed only to a matching identity | a manager test: a grant whose account identity differs from the bound licence's is refused and a notification emitted |
|
||||||
|
| An unservable binding refuses rather than lends | a manager test: a worker bound to a dead licence is answered with a refusal, never another licence's token |
|
||||||
|
| A switch through the console changes the token on the node and nothing in the answer is a token | a live check on one workstation |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0024](0024-model-access-is-a-provision.md), [ADR 0050](0050-model-access-is-vendor-agnostic.md) — the licence as a named thing, the carve-out this moves with the manager
|
||||||
|
- [ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md) — why the seat is named for the vendor
|
||||||
|
- [ADR 0113](0113-the-vault-makes-every-secret.md) — the vault's custody of the manager's key, and the exception stated here
|
||||||
|
- [ADR 0126](0126-a-module-declares-its-own-seats.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md) — a module's seat, its verbs, a call addressed to one machine
|
||||||
|
- [to-be 32 §10](../03-DESIGN/01-to-be/32-what-a-module-declares.md) — a secret on the bus
|
||||||
|
- [to-be 36](../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md), [to-be 39](../03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md) — the two modules
|
||||||
|
- the predecessor's `claude-licences` and `claude-code` modules, read 2026-10-02: the lease, the floor, the lineage comparison, the identity guard, the touchpoints
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0005-the-node-host.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 184. A service the mesh asked to run is still running a moment later
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The host already refuses to take a service manager's word for it. Three places in one function read
|
||||||
|
a unit back after acting on it, each with a comment saying why: *a service manager accepting a
|
||||||
|
command says the transaction was accepted, not that the unit is running — one that starts and
|
||||||
|
immediately dies satisfies it.* The intent was right and the implementation did not reach it.
|
||||||
|
|
||||||
|
On 2026-10-02 the mesh composed a fail2ban jail whose pattern the daemon refused. The host wrote the
|
||||||
|
files, restarted the service, read the unit back and reported *restarted*. The unit was `active` at
|
||||||
|
that instant and `failed` 221 milliseconds later, which the unit's own record states. Both public
|
||||||
|
machines then kept no bans at all — every jail, not the one at fault — and nothing in the mesh said
|
||||||
|
so. The fault was found by calling a tool that needed the daemon, not by the mesh noticing.
|
||||||
|
|
||||||
|
The read-back races the failure. A service manager returns when it has started the process; a daemon
|
||||||
|
that reads its configuration, refuses it and exits does so a fraction of a second afterwards. One
|
||||||
|
look sees `activating` or `active` whatever the process is about to do, and *the host reports success
|
||||||
|
for a machine that is already wrong* — the one shape of failure this host exists to refuse
|
||||||
|
([ADR 0005](0005-the-node-host.md)).
|
||||||
|
|
||||||
|
A command the module declares — *test the configuration before restarting* — was considered and
|
||||||
|
rejected. The link carries no actions ([ADR 0005](0005-the-node-host.md)), and a verification
|
||||||
|
command is a command: a declaration that carried one would be remote execution over the bus,
|
||||||
|
arriving as root on every machine, which is a far larger door than the fault it closes. The host
|
||||||
|
does not need one. It already knows what it asked for.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. A unit the host has just asked to run is read twice**, with a pause between the reads long
|
||||||
|
enough for a daemon that refuses its configuration to have exited. Not running at the second look is
|
||||||
|
a failure of that resource, named with the unit and the state it is in — the same failure the single
|
||||||
|
read was always meant to catch.
|
||||||
|
|
||||||
|
**2. It is never a wait for a unit to come up.** A unit still starting reads as running at both
|
||||||
|
looks and is accepted, exactly as before. What the second look catches is a unit that *was* running
|
||||||
|
and is not any more. A service asked to be stopped is not waited on at all.
|
||||||
|
|
||||||
|
**3. The host tests nothing and runs nothing of a module's.** The second look is the host checking
|
||||||
|
the state it was told to establish, which is its whole job; the declaration gains no vocabulary, and
|
||||||
|
no command reaches a machine that did not already come from a built artifact.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Every apply that starts, restarts or reloads a service spends a moment confirming it. The cost is
|
||||||
|
bounded by the number of services that changed in that apply, which is usually none.
|
||||||
|
- A module whose configuration the mesh composes — the packet filter, the intrusion prevention, the
|
||||||
|
resolver — now fails its apply when the composition is bad, instead of reporting success onto a
|
||||||
|
dead daemon. `status` names the machine, which is how the operator finds out.
|
||||||
|
- It does not prevent the bad composition. [ADR 0179](0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md)'s
|
||||||
|
manifest check is what refuses the one that caused this, at merge time; this record is what makes
|
||||||
|
the *next* one visible within a minute rather than invisible until something asks the daemon a
|
||||||
|
question.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A unit that is running at the first look and dead at the second fails the apply, naming the unit and its state | a host test over a service manager that answers as systemd does |
|
||||||
|
| A unit still starting is accepted at both looks | a host test |
|
||||||
|
| A service asked to be stopped is not waited on | a host test |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0005](0005-the-node-host.md), [ADR 0179](0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md)
|
||||||
|
- [Design 05 — The node host](../03-DESIGN/01-to-be/05-the-node-host.md)
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 185. A control plane behind its seat's row serves what it can
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The mesh's own verbs are the controller seat's tools, and the seat's row is the store's
|
||||||
|
([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)). A control plane reads the
|
||||||
|
row at start and installs a handler per verb; a verb the row carries that the binary cannot run was
|
||||||
|
refused at start rather than at the first call, so that a disagreement between the row and the
|
||||||
|
binary was said early. The refusal aborted the start.
|
||||||
|
|
||||||
|
On 2026-10-02 a merge added one verb. The new control plane started, widened the row, and ran. A
|
||||||
|
push a few seconds later recreated its container at the previous image — a stale declaration from
|
||||||
|
an overlapping wave, [issue 201](../04-ISSUES/201-a-push-recreated-the-controller-behind-the-row-its-successor-wrote/00-report.md) —
|
||||||
|
and the older binary read a row naming a word it had never heard. It refused to start, and kept
|
||||||
|
refusing. The mesh had no voice for ten minutes: no verb answered, no node could be pushed, no build
|
||||||
|
was dispatched, and `status` said nothing because `status` is one of the verbs that had stopped
|
||||||
|
being served. The way back was a person running the binary by hand outside its service, because the
|
||||||
|
push that would have replaced it is itself a verb of the control plane that was down.
|
||||||
|
|
||||||
|
The check was right about the fact and wrong about the cost. A row ahead of a binary is the ordinary
|
||||||
|
state of a roll-out: the row is widened by whichever control plane starts first, and a mesh with one
|
||||||
|
control plane sees that gap on every merge that adds a verb. Making it fatal turned a transient into
|
||||||
|
an outage with no path out that did not need a human.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. A control plane serves the verbs it can run and does not refuse to start for the ones it
|
||||||
|
cannot.** The row remains the authority on what the seat serves; this is only about what this binary
|
||||||
|
does when it is behind the row.
|
||||||
|
|
||||||
|
**2. A verb it cannot run answers the reason.** Not silence and not a missing subject: a caller gets
|
||||||
|
a sentence naming the verb, saying this control plane cannot run it and that it is a verb of a newer
|
||||||
|
build. A verb that is simply absent from the row is still not served at all — that is the row
|
||||||
|
deciding, which is unchanged.
|
||||||
|
|
||||||
|
**3. It says so once at start**, naming every verb of the row it cannot run, so the gap is visible
|
||||||
|
in the log of the thing that has it rather than only at the moment somebody calls one.
|
||||||
|
|
||||||
|
**4. A mesh with no controller seat at all is still a refusal.** That is not a version gap, it is a
|
||||||
|
mesh that has not been seeded, and nothing this control plane does would be meaningful.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- An overlapping roll-out costs the verbs the newer build added, for as long as the older binary is
|
||||||
|
in place. Everything else — every push, every build, every read — keeps working, and the ordinary
|
||||||
|
machinery that notices a machine is behind is what puts the newer binary back.
|
||||||
|
- The log gains one line on a control plane that is behind, and nothing on one that is not.
|
||||||
|
- Issue 201's other half remains: the push that sent a stale declaration is a race worth closing on
|
||||||
|
its own terms. This record makes that race survivable rather than fatal, which is the difference
|
||||||
|
between a transient and an outage, and is deliberately the cheaper half.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A row carrying a verb this build cannot run still serves every verb it can, and names the one it cannot | a controller test over a widened row |
|
||||||
|
| The unknown verb answers a sentence naming itself and saying this build is behind | the same test |
|
||||||
|
| A mesh with no controller seat is refused | the existing start-up path |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md), [ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md)
|
||||||
|
- [Issue 201](../04-ISSUES/201-a-push-recreated-the-controller-behind-the-row-its-successor-wrote/00-report.md)
|
||||||
|
- [Design 33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 186. A ban list never holds a neighbour, and the mesh's own bans are its own wherever they hang
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0179](0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md) gave the
|
||||||
|
public proxy a jail. Within the hour the home server's ban list held `192.168.1.1` — the house's own
|
||||||
|
router. The router reflects local traffic, so every client in the building reaches that machine as
|
||||||
|
the gateway's address; one local request for a name the mesh does not serve, three times in a day,
|
||||||
|
and the whole house is refused by the machine it was asking. The jails inherited an `ignoreip` of
|
||||||
|
the loopback and the mesh's own range, which was right when the only jail read the ssh daemon and
|
||||||
|
the only clients were the mesh's; a jail on a public front door sees the neighbours too.
|
||||||
|
|
||||||
|
The same jail broke the other half of [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md).
|
||||||
|
The home server began reading *NOT the mesh alone: 1 rule set the mesh did not write refuses traffic
|
||||||
|
here*, and the rule set named was the mesh's own ban chain, written by the mesh's own intrusion
|
||||||
|
prevention minutes earlier. The host's reader of the legacy filter required every path into a chain
|
||||||
|
of refusals to come from a built-in chain whose policy accepts, before it would call that chain a
|
||||||
|
ban. On that machine the chain hangs off the container runtime's user chain as well as the input
|
||||||
|
chain, and the runtime had set the forward policy to DROP — so the mesh reported its own work as a
|
||||||
|
foreigner's, on the one machine where the group's exit condition was supposed to hold.
|
||||||
|
|
||||||
|
Both faults are one mistake in two places: a rule written about the public internet, applied to
|
||||||
|
everything that arrives.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. A ban list never holds a neighbour.** The jails the mesh composes never ban a source on a
|
||||||
|
private range — the mesh's own range, which was already named rather than written
|
||||||
|
([ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md)), and every address space
|
||||||
|
reserved for private use beside it, in both families. A machine behind a router that reflects local
|
||||||
|
traffic sees its whole building as one address; a ban there is a self-inflicted outage, and the
|
||||||
|
sources worth banning are not on those ranges in the first place.
|
||||||
|
|
||||||
|
**2. The mesh's own bans are its own wherever they hang.** A chain of refusals is a ban list when
|
||||||
|
every refusal names the sources it refuses and the chain accepts nothing — the rule the host already
|
||||||
|
applied to the packet filter's own tables, now applied to the legacy filter too, and nothing more.
|
||||||
|
The policy of the chains that jump into it says nothing about what it is: that policy is already
|
||||||
|
classified where it belongs, as the container runtime's, and requiring it here counted it twice.
|
||||||
|
|
||||||
|
**3. A chain that accepts anything is still not a ban.** That is what keeps a predecessor's
|
||||||
|
allow-these-and-drop-the-rest chain classified as something an operator must look at, which is the
|
||||||
|
distinction [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md) exists to draw.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The composed jails gain the private ranges in their never-ban list. An address already banned
|
||||||
|
stays banned until it is released; the house's router was released by hand the moment it was found.
|
||||||
|
- The home server reads *the mesh alone* again, which is group 7's exit condition and was false for
|
||||||
|
about an hour.
|
||||||
|
- A machine whose apply fails for an unrelated reason does not revisit its found firewall's record
|
||||||
|
at all — the step runs only after a clean apply ([ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)).
|
||||||
|
The home server's record therefore still reads *retired by the mesh* although the front end is
|
||||||
|
uninstalled, and will correct itself once that machine's own stuck module is fixed. It is a stale
|
||||||
|
record, not a wrong machine.
|
||||||
|
- The record number the front end's removal was given moved under it: another session took 0175
|
||||||
|
while that record was in review, and it is now
|
||||||
|
[ADR 0180](0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md). The citations
|
||||||
|
the host and the control plane print were pointing at an unrelated record and are corrected here.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A private source is never banned | the module's jail configuration, read back by `fail2ban.fail2ban_settings` on a machine |
|
||||||
|
| The mesh's own ban chain reads as a ban behind a dropping forward policy | a host test over the home server's own captured rule set |
|
||||||
|
| A chain that accepts anything is not a ban | a host test |
|
||||||
|
| Live | done 2026-10-02: all four machines read *the mesh alone*, the home server counting its own ban chain as a ban; no ban held anywhere is a private address |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0179](0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md), [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0180](0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md)
|
||||||
|
- [Design 08 — Connectivity](../03-DESIGN/01-to-be/08-connectivity.md), [Design 31 — A module declares its fail2ban jail](../03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md)
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 187. A dead tracker is not the machine's failure
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The home server had not applied a declaration cleanly since midday. One run-once step — the one
|
||||||
|
that writes a media app's download clients and indexers through the app's own API — exited
|
||||||
|
non-zero, forty-nine times over six hours, for one public tracker that had stopped answering. The
|
||||||
|
step's own words: the entry was *written*, and the app's test of it then failed with a 400 from the
|
||||||
|
indexer proxy. The machine reported *not doing what it was told* for the rest of the day.
|
||||||
|
|
||||||
|
What that gated matters more than the step. A converged machine retires the firewall it was found
|
||||||
|
with only after a clean apply ([ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)),
|
||||||
|
so that machine went on recording its found front end as merely *retired* long after the package
|
||||||
|
had been uninstalled ([ADR 0180](0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md)).
|
||||||
|
A dead public tracker was holding a firewall record hostage, which is not a connection anybody
|
||||||
|
would design.
|
||||||
|
|
||||||
|
The step already knew this was not its business. It had a rule for exactly this: an entry the mesh
|
||||||
|
only *found and re-pointed*, rather than one it was told to make, whose feed is gone, is said and
|
||||||
|
left as found — *failing the node's apply on every heartbeat for it reports the mesh as wrong about
|
||||||
|
a tracker*. The rule was there and matched one shape of the fault. An app can refuse to save such an
|
||||||
|
entry, and it can save it and then fail its own test; saving validates settings, and the test runs a
|
||||||
|
live search. The rule caught the first and let the second through.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. An entry the mesh only found is never the machine's failure.** Whatever shape the app's
|
||||||
|
refusal takes — it would not save it, or it saved it and its own test fails — an indexer the mesh
|
||||||
|
found and re-pointed is reported as a notice and left as found. What decides is whose entry it is,
|
||||||
|
not which sentence the app returned.
|
||||||
|
|
||||||
|
**2. What the mesh is answerable for is the plumbing.** That the entry exists, points at this
|
||||||
|
mesh's indexer proxy, and carries the credential the mesh delivered — which was checked against the
|
||||||
|
proxy before anything was written. Whether a public tracker answers today is not the mesh's to
|
||||||
|
promise, and a machine that reports itself broken because one did is lying about itself.
|
||||||
|
|
||||||
|
**3. An entry the operator listed is theirs to insist on.** An indexer named in the step's settings
|
||||||
|
is one the mesh was told to make, and it still fails the step when it cannot be made to work. The
|
||||||
|
notice says so, and says that listing the indexer is how to turn it back into a failure.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The home server applies cleanly again, and everything a clean apply gates — its found firewall's
|
||||||
|
record among it — follows.
|
||||||
|
- A tracker that dies is a line in a report rather than a machine that reads as broken. An operator
|
||||||
|
who wants it gone removes the entry or repairs the feed; the mesh says which, every time it runs.
|
||||||
|
- The four Servarr modules carry one byte-identical copy of this step each
|
||||||
|
([ADR 0069](0069-a-module-is-a-repository-and-a-path.md)), so the change lands in four places and
|
||||||
|
a test refuses any drift between them.
|
||||||
|
- It does not widen to a download client: one the mesh was told to write and cannot is still a
|
||||||
|
failure, because the mesh chose it and nothing else will fix it.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A found feed whose tracker answers an error after the entry was written is a notice | the step's tests, with the home server's own message and the app's two validations modelled apart |
|
||||||
|
| An indexer the settings list is still a failure | the same test |
|
||||||
|
| The four copies of the step do not drift | the step's own sameness test |
|
||||||
|
| Live | done 2026-10-02: the home server applies cleanly after six hours of failing, `status` holds no machine wrong or behind, and its found firewall reads *removed* |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0136](0136-a-step-gates-its-module-not-the-machine.md), [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), [ADR 0180](0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md), [ADR 0069](0069-a-module-is-a-repository-and-a-path.md)
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 188. A module's own code is bundles in any language, and a tools bundle speaks MCP to the runtime
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) put one
|
||||||
|
tool runtime on every node and said a module brings its tools as a bundle. The runtime that exists
|
||||||
|
is written in TypeScript and brings a bundle to life by **importing it into its own process**, which
|
||||||
|
only JavaScript can be. The SDK ([ADR 0039](0039-what-the-sdk-holds-and-refuses.md)) is one
|
||||||
|
TypeScript package. The builder knows three toolchains — TypeScript, Go, Python — and every one of
|
||||||
|
the 35 catalogue modules with tools wraps them in a container on the runtime's TypeScript image.
|
||||||
|
Nothing in the records says a module's code may be written in anything else, and nothing refuses a
|
||||||
|
module that wraps its own code in an image to get around that.
|
||||||
|
|
||||||
|
The operator's direction, stated on 2026-10-02 and repeated: *the SDK is the most important part;
|
||||||
|
we must not limit developers; tools can be written in any possible language — Rust, C, Go,
|
||||||
|
JavaScript. A service in Go or Rust as a systemd unit must be possible too. One module can deliver
|
||||||
|
all kinds of bundles: one for its tools, one for a seat's implementation, one for a daemon. Support
|
||||||
|
the bare minimum first, as a skeleton; a full implementation comes when the work requires it.*
|
||||||
|
|
||||||
|
Measured against that: the `bundle` artifact kind already names a language and the `process`
|
||||||
|
resource already runs a command from an unpacked bundle as a unit the host writes
|
||||||
|
([ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)), so a Go
|
||||||
|
daemon as a native service is possible today and one module in the catalogue does it. What is not
|
||||||
|
possible is a tool in any language but one, and what is not written is that any of this is the
|
||||||
|
rule.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **One SDK, one language, as now.** Rejected: it limits who can write a module to one
|
||||||
|
ecosystem, which the operator declines, and it is what made every module's tools a container
|
||||||
|
on one image.
|
||||||
|
2. **A full bus client per language.** Each SDK speaks the bus itself; the runtime only
|
||||||
|
supervises. Rejected: a transport in every SDK is what ADR 0039 refuses, and a bus change
|
||||||
|
would then rebuild every module in every language — the cascade, multiplied.
|
||||||
|
3. **A tools bundle is a process the runtime launches and speaks a small local protocol to,
|
||||||
|
and that protocol is MCP over stdio.** Chosen. The runtime already speaks MCP outward (the
|
||||||
|
console); speaking it inward to a child process is the same vocabulary. Every language that
|
||||||
|
has an MCP server library can write a tools bundle today with no mesh SDK at all, and the
|
||||||
|
mesh's own SDK for a language is a thin convenience over it. The transport stays in the
|
||||||
|
runtime, so a bus change rebuilds nothing.
|
||||||
|
4. **A protocol of the mesh's own design.** Rejected: a second way to describe a tool, its
|
||||||
|
schema and its call, inventing what MCP already settled, for no gain.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. A module's own code is bundles, in any language the mesh has a toolchain for, and never an
|
||||||
|
image.** A `bundle` names its language and what it is for. Images are for third-party software a
|
||||||
|
module installs — a database, a forge — never for code the module wrote. One module may declare
|
||||||
|
several bundles: its tools, its implementation of a seat's verbs, a daemon, a step. Each is built
|
||||||
|
alone and delivered alone, as [ADR 0156](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)
|
||||||
|
already has it.
|
||||||
|
|
||||||
|
**2. A bundle the runtime serves is a process that speaks MCP over stdio.** The node's runtime
|
||||||
|
launches it as the bundle names it — an interpreter and a file, or a binary — with the runtime's
|
||||||
|
environment, asks `tools/list`, and answers each call on the bus by `tools/call`. A tool whose name
|
||||||
|
is `<seat>.<verb>` is the module's implementation of that seat's verb; any other name is the
|
||||||
|
module's own tool. Everything the runtime does with what it is told — subjects from the membership,
|
||||||
|
a held seat's verbs, the `tools` answer, a bundle that fails named and the others serving — stays as
|
||||||
|
[ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) and
|
||||||
|
[ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||||
|
have it. A TypeScript bundle may still be imported into the runtime's own process; that is a
|
||||||
|
shortcut over the same contract, not a second contract, and a TypeScript bundle written against
|
||||||
|
the protocol is served the same way as any other.
|
||||||
|
|
||||||
|
**3. A bundle that is a service is a `process`**, run by the host as a unit, in whatever language it
|
||||||
|
is compiled from, exactly as the host's own bundle already is. Nothing new is decided here; it is
|
||||||
|
said so that it is the rule and not an example.
|
||||||
|
|
||||||
|
**4. One thin SDK per language, and the test of ADR 0039 applies to each.** An SDK for a language
|
||||||
|
holds the MCP-over-stdio loop, the tool-definition type and the few primitives a module's code
|
||||||
|
needs; it holds no transport, no module's client and nothing volatile. Where a language has a
|
||||||
|
sound MCP library, the SDK wraps it rather than re-implementing it. The languages are those that
|
||||||
|
make sense to write a module in; the first set is TypeScript, Go, Python, Rust and C, and the set
|
||||||
|
grows when a module needs one, not before.
|
||||||
|
|
||||||
|
**5. Skeleton first.** Each piece — a toolchain, a launcher, an SDK — exists at the bare minimum
|
||||||
|
that lets one bundle in that language be built, delivered and answer one tool on the live mesh.
|
||||||
|
Anything beyond that is added when a module needs it. A skeleton that is not proven by one bundle
|
||||||
|
answering is not a skeleton; it is a promise.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The runtime gains a launcher beside its loader. The loader, the memberships, the seats and the
|
||||||
|
failure handling built for ADR 0175 stand; the launcher is the one new step.
|
||||||
|
- The builder gains a toolchain per language, each at the skeleton: compile, pack, name the
|
||||||
|
entrypoint. Rust and C are new; a language that compiles to a binary says its operating system
|
||||||
|
as a Go bundle already does.
|
||||||
|
- An existing MCP server in any language is already a valid tools bundle. What the mesh adds is
|
||||||
|
the subjects, the seats and the memberships around it.
|
||||||
|
- The gate [to-be 38](../03-DESIGN/01-to-be/38-building-the-operators-machine.md) WP2 adds —
|
||||||
|
refusing a tools container built on the runtime's image — widens: a module whose own code is
|
||||||
|
an image artifact is refused at registration, naming this record.
|
||||||
|
- What got harder: a tools bundle is now a process per module on the node rather than code in
|
||||||
|
one process, so the runtime supervises children and restarts one that dies. The one-process
|
||||||
|
shape ADR 0175 counted on for the TypeScript shortcut remains available for it.
|
||||||
|
- ADR 0039's "what the SDK holds" now reads per language; its refusals are unchanged and are the
|
||||||
|
reason option 2 was rejected.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A module's own code is never an image | the catalogue's registration check: a manifest with a `bundle` kind of own code *and* an image artifact built from the module's own directory is refused, naming this record |
|
||||||
|
| A tools bundle in a language other than TypeScript answers on the bus | the runtime's tests: a bundle written against the protocol in a second language, launched, its tool called over a real bus |
|
||||||
|
| A TypeScript bundle written against the protocol is served like any other | the same tests, with the TypeScript shortcut off |
|
||||||
|
| Each SDK is thin | each SDK's own README states what it holds under ADR 0039's test, and its size is in the mesh's records |
|
||||||
|
| Live | a tool in a compiled language answers from the node's runtime on one machine |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md),
|
||||||
|
[ADR 0039](0039-what-the-sdk-holds-and-refuses.md),
|
||||||
|
[ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md),
|
||||||
|
[ADR 0156](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md),
|
||||||
|
[ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||||
|
- [To-be 38](../03-DESIGN/01-to-be/38-building-the-operators-machine.md) — the work packages this
|
||||||
|
record widens
|
||||||
|
- The Model Context Protocol's stdio transport — the local protocol a tools bundle speaks
|
||||||
@@ -168,6 +168,26 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0132** — [A seat carries the tools its holder must serve](0132-a-seat-carries-the-tools-its-holder-must-serve.md)
|
- **0132** — [A seat carries the tools its holder must serve](0132-a-seat-carries-the-tools-its-holder-must-serve.md)
|
||||||
- **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)
|
||||||
|
- **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)
|
||||||
|
- **0179** — [The intrusion seat serves its verbs, a container may log to the journal, and every door declares its jail](0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.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)
|
||||||
|
- **0184** — [A service the mesh asked to run is still running a moment later](0184-a-service-the-mesh-asked-to-run-is-still-running-a-moment-later.md)
|
||||||
|
- **0185** — [A control plane behind its seat's row serves what it can](0185-a-control-plane-behind-its-seats-row-serves-what-it-can.md)
|
||||||
|
- **0186** — [A ban list never holds a neighbour, and the mesh's own bans are its own wherever they hang](0186-a-ban-list-never-holds-a-neighbour.md)
|
||||||
|
- **0187** — [A dead tracker is not the machine's failure](0187-a-dead-tracker-is-not-the-machines-failure.md)
|
||||||
|
|
||||||
### Its tiers, from the bottom up
|
### Its tiers, from the bottom up
|
||||||
|
|
||||||
@@ -256,6 +276,16 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0146** — [Connectivity is checked by name, per hosting form, with a valid certificate](0146-connectivity-is-checked-by-name-per-hosting-form.md)
|
- **0146** — [Connectivity is checked by name, per hosting form, with a valid certificate](0146-connectivity-is-checked-by-name-per-hosting-form.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)
|
- **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)
|
||||||
|
- **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)
|
||||||
|
- **0181** — [The operator account is a node fact, and a home is a placement root](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)
|
||||||
|
- **0182** — [Inside a home, the mesh owns the directory and the files it places, writes into the tool's own files, and holds everything else as found](0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
||||||
|
- **0183** — [The Anthropic licence manager is a module holding a seat; it hands each node's agent its token over the bus, sealed; the controller and the host have no part](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
|
||||||
|
- **0188** — [A module's own code is bundles in any language, and a tools bundle speaks MCP to the runtime](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)
|
||||||
|
|
||||||
### How it is built
|
### How it is built
|
||||||
|
|
||||||
@@ -277,6 +307,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
|
||||||
|
|
||||||
@@ -297,5 +328,6 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0034** — [The local account owns the mesh, and a web application's login is not that](0034-the-local-account-owns-the-mesh.md)
|
- **0034** — [The local account owns the mesh, and a web application's login is not that](0034-the-local-account-owns-the-mesh.md)
|
||||||
- **0080** — [The development cycle is checked, not trusted](0080-the-development-cycle-is-checked.md)
|
- **0080** — [The development cycle is checked, not trusted](0080-the-development-cycle-is-checked.md)
|
||||||
- **0081** — [A decision nothing cites is not yet in the chain](0081-a-decision-nothing-cites-is-not-yet-in-the-chain.md)
|
- **0081** — [A decision nothing cites is not yet in the chain](0081-a-decision-nothing-cites-is-not-yet-in-the-chain.md)
|
||||||
|
- **0153** — [The record is read by a module the mesh assigns, and the console lists it](0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)
|
||||||
|
|
||||||
<!-- index:end -->
|
<!-- index:end -->
|
||||||
|
|||||||
@@ -1,78 +1,58 @@
|
|||||||
---
|
---
|
||||||
layer: as-is
|
layer: as-is
|
||||||
status: implemented
|
status: implemented
|
||||||
code: [hal]
|
code: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
|
||||||
updated: 2026-08-23
|
updated: 2026-09-30
|
||||||
decisions: []
|
decisions:
|
||||||
|
- 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
|
||||||
|
- 02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md
|
||||||
|
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# Knowledge
|
# Knowledge
|
||||||
|
|
||||||
The mesh keeps two knowledge stores. They are not redundant, and knowing which is which is the
|
**The mesh keeps no knowledge store.** What it knows is what its modules answer, and the way a person
|
||||||
difference between finding an answer in one search and rediscovering it over several hours.
|
or an agent asks is the console's tool list on the machine they sit at
|
||||||
|
([13 — The console](13-the-console.md)). This document used to describe two stores; it is rewritten
|
||||||
|
because neither exists from the mesh's side, and an as-is document that describes what is gone is a
|
||||||
|
brochure.
|
||||||
|
|
||||||
## The operational memory
|
## What was here, and where it went
|
||||||
|
|
||||||
A store of operational notes, written and read by whoever — human or agent — is working. Each
|
Until the cut-over of 2026-09-28 the predecessor ran two stores: an operational memory of notes
|
||||||
note is a slug and a body: how something works, what went wrong, what the fix was, what
|
indexed on symptoms, and a structured archive of governed documents with a librarian approving
|
||||||
assumption turned out to be false.
|
promotion. Both were reached through the predecessor's tool server over the bus the mesh removed
|
||||||
|
([issue 147](../../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
|
||||||
|
Nothing in the mesh reaches them now, and nothing in the mesh has replaced them: there is no note
|
||||||
|
store, no archive, no librarian, and the lessons of the last days were written into this repository by
|
||||||
|
hand. That is a gap, and it is stated here rather than papered over. What replaces a symptom-indexed
|
||||||
|
memory, if anything does, is undecided.
|
||||||
|
|
||||||
It is indexed on **symptoms**. The entry someone needs is usually titled after the error they
|
## The record
|
||||||
are staring at, which is why the standing instruction is to search the literal error text
|
|
||||||
before forming a hypothesis rather than after one fails.
|
|
||||||
|
|
||||||
Its content is overwhelmingly the record of previous debugging: a large body of
|
**The design record is read where it is written.** Since 2026-09-30 a module, `records`, keeps a
|
||||||
troubleshooting entries, module conventions, and standing notes about work that is open. It is
|
checkout of this repository from the forge — cloned from the `git` seat, reset to the origin on every
|
||||||
the mesh's institutional memory of *what has already gone wrong*.
|
merge the forge announces and every ten minutes — and answers over the bus: where a phrase appears as
|
||||||
|
written, one document whole, what a folder holds, and where the checkout stands, each naming the
|
||||||
|
commit it read. Which repository it reads is a setting on its assignment; the module names no mesh.
|
||||||
|
|
||||||
The cost of skipping it is documented in the mesh's own record: entries have been rediscovered
|
It is listed by the console beside every other tool, with a description that says to search the
|
||||||
from scratch, over hours, in sessions where the search was skipped because the trail felt
|
literal words of a symptom before forming a hypothesis. That is what
|
||||||
confident. It fires hardest on familiar ground, not unfamiliar ground.
|
[ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md) meant by *beside
|
||||||
|
everything else*, in a mesh with no store to be beside
|
||||||
|
([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)).
|
||||||
|
|
||||||
## The structured archive
|
Nothing is copied. A checkout lags the source by seconds after an announced merge and by minutes
|
||||||
|
otherwise, and says so.
|
||||||
|
|
||||||
A second store, structured rather than flat: spaces, pages, revisions, tiers, and full-text
|
## The constitution
|
||||||
search. Where the operational memory is a note, this is a document with an owner and a
|
|
||||||
lifecycle.
|
|
||||||
|
|
||||||
Content is promoted through tiers — private, then team, then platform — with a librarian agent
|
[`00-META/how-we-build.md`](../../00-META/how-we-build.md) is the source of the mesh constitution
|
||||||
owning approval and promotion at the boundary. Proposals to edit are reviewed rather than
|
([ADR 0021](../../02-DECISIONS/0021-hq-is-the-source-of-the-constitution.md)). The page it used to be
|
||||||
applied.
|
synchronised into lived in the predecessor's archive and is unreachable; the constitution today is
|
||||||
|
read from this repository, through the same module, and playbook 05's sync has nothing to write to.
|
||||||
This is where the mesh's **governed** documents live, including the constitution injected into
|
|
||||||
design sessions ([ADR 0020](../../02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md)).
|
|
||||||
|
|
||||||
## Why both
|
|
||||||
|
|
||||||
The distinction is by lifecycle, not by subject.
|
|
||||||
|
|
||||||
| Operational memory | Structured archive |
|
|
||||||
|---|---|
|
|
||||||
| Written the moment something is learned | Written deliberately, reviewed |
|
|
||||||
| Flat, symptom-indexed | Structured, tiered, owned |
|
|
||||||
| Anyone writes; nothing approves | Promotion is approved |
|
|
||||||
| Truth is "this happened" | Truth is "this is agreed" |
|
|
||||||
|
|
||||||
Collapsing them would cost one of the two properties: either every hard-won note waits for
|
|
||||||
review, or governed documents can be changed by anyone mid-incident.
|
|
||||||
|
|
||||||
## Where this repository sits
|
## Where this repository sits
|
||||||
|
|
||||||
This repository is a third thing, and the objection was raised when it was created: a fourth
|
A third thing beside two that are gone, which makes it the first: the one governed record the mesh
|
||||||
knowledge system repeats the mistake the split was made to fix.
|
has, public, read by a module the mesh assigns, and edited nowhere else.
|
||||||
|
|
||||||
The answer given was **indexing, not location** — that these documents are indexed into the
|
|
||||||
knowledge base so that a symptom search returns them alongside everything else. One source,
|
|
||||||
many surfaces.
|
|
||||||
|
|
||||||
**That indexing does not currently exist.** A search for this repository's content returns
|
|
||||||
nothing. The claim is load-bearing for the decision to separate the repository at all, and
|
|
||||||
until it is true, this repository is exactly the fourth knowledge system the objection
|
|
||||||
described. Recorded here because it is a statement about how the mesh's knowledge actually
|
|
||||||
works today, and as [`04-ISSUES/006`](../../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md).
|
|
||||||
|
|
||||||
## The librarian
|
|
||||||
|
|
||||||
A single agent owns the archive's approvals and promotions. Its approval capabilities have at
|
|
||||||
times not been reachable as tools, which does not affect the operational memory but does mean
|
|
||||||
promotion stops silently — the store keeps accepting proposals that nothing can approve.
|
|
||||||
|
|||||||
@@ -82,6 +82,22 @@ to clone.
|
|||||||
The schema column added for this defaults to empty rather than null, because "not on a seat" is a
|
The schema column added for this defaults to empty rather than null, because "not on a seat" is a
|
||||||
real answer, so every row recorded before the change keeps exactly the meaning it had.
|
real answer, so every row recorded before the change keeps exactly the meaning it had.
|
||||||
|
|
||||||
|
## A seat's protocol is on its row, and the controller serves its own
|
||||||
|
|
||||||
|
*Since 2026-09-30 ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)).*
|
||||||
|
The `seat` table carries `accepts`, `emits` and `serves`; `serves` holds each verb with its description
|
||||||
|
and input schema. The rows were seeded from the compiled defaults the first time a controller with the
|
||||||
|
columns migrated, and each later migration adds any verb the defaults name that a row lacks, never
|
||||||
|
removing one. `UseSeats` still falls back to the compiled protocol for a row with none, which after the
|
||||||
|
first seeding is no row.
|
||||||
|
|
||||||
|
The `mesh-controller` seat serves twelve verbs — `tools`, `status`, `nodes`, `node`, `modules`, `seats`,
|
||||||
|
`builds`, `plan`, `assign`, `unassign`, `push`, `build` — on `mesh.seat.mesh-controller.tool.<verb>`,
|
||||||
|
each answered by the controller running that command in its own binary and returning what it printed.
|
||||||
|
A module claiming a mesh seat with verbs must list them under `tools` or registration refuses it by
|
||||||
|
name. A node-scoped seat's tool is `mesh.seat.<seat>.tool.<verb>.<node>`; no node-scoped seat declares
|
||||||
|
one yet.
|
||||||
|
|
||||||
## Where this differs from the design
|
## Where this differs from the design
|
||||||
|
|
||||||
**Capacity is not implemented.** The design's vocabulary has a seat with a capacity, and a
|
**Capacity is not implemented.** The design's vocabulary has a seat with a capacity, and a
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
layer: as-is
|
||||||
|
status: implemented
|
||||||
|
code: [mesh-catalog modules/mesh-console, mesh-tools src/mesh.ts, mesh-tools src/http.ts, mesh-tools src/runtime.ts, mesh-controller internal/broker, mesh-controller cmd/mesh-controller/check.go, mesh-controller cmd/mesh-controller/seatverbs.go]
|
||||||
|
updated: 2026-09-30
|
||||||
|
decisions:
|
||||||
|
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
||||||
|
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
|
||||||
|
- 02-DECISIONS/0037-where-a-module-lives.md
|
||||||
|
- 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# The console, as it runs
|
||||||
|
|
||||||
|
**The mesh's tools reach a person through a module the mesh assigned to their machine.** Since
|
||||||
|
2026-09-30 a workstation that is a node can be assigned `mesh-console`; the mesh mints a bus account
|
||||||
|
`<node>.mesh-console`, seals its credential to the machine, and the container binds
|
||||||
|
`127.0.0.1:<port>` with the port the mesh assigned for the manifest's declared one. An agent on the
|
||||||
|
machine is pointed at `http://127.0.0.1:<port>/mcp` and sees the mesh's tools; a person uses the same
|
||||||
|
endpoint. Nothing on the machine holds a credential a person had to carry.
|
||||||
|
|
||||||
|
## What it answers
|
||||||
|
|
||||||
|
`initialize`, `tools/list`, `tools/call`, over HTTP, one JSON body per request, no session and no event
|
||||||
|
stream. `tools/list` is what the running modules answered: every tool runtime built on or after that day
|
||||||
|
serves a `tools` verb for its module, and the console asks the catalogue for the roster and each module
|
||||||
|
for its tools. A module that did not answer is named in the list's `_meta.notAnswering`. On the day it
|
||||||
|
shipped that was 36 of 51 modules — those that serve no tools at all, and those whose rebuilt runtime the
|
||||||
|
mesh records rather than rolls out — and 62 tools from the rest.
|
||||||
|
|
||||||
|
`tools/call` reaches any tool by `<module>.<tool>`, listed or not. The console's grant is `*`, so what it
|
||||||
|
may call is every tool on the mesh; its account may publish nothing else and subscribes nothing.
|
||||||
|
|
||||||
|
## The mesh's own verbs
|
||||||
|
|
||||||
|
*Since 2026-09-30 evening ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)).*
|
||||||
|
The console asks the `mesh-controller` seat's `tools` verb beside the modules and lists every role's
|
||||||
|
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
|
||||||
|
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. The
|
||||||
|
`mesh-controller` *module* is always named as not answering: it serves no module tools, only its seat's.
|
||||||
|
|
||||||
|
## Around it
|
||||||
|
|
||||||
|
- **`invokes`** in a manifest is the grant. It is composed into the bus's user list exactly as a
|
||||||
|
person's account is; the console is the only module that declares it.
|
||||||
|
- **`module check <file|dir>…`** on the controller's binary judges a manifest with no mesh: the strict
|
||||||
|
parse, every per-manifest problem, and the rules between the manifests given. It prints what it cannot
|
||||||
|
judge without a store rather than refusing. The console's own manifest was the first thing checked
|
||||||
|
with it, and the whole catalogue passes.
|
||||||
|
- **The person's client remains.** `operator issue` and `mesh tools|call|mcp` with a credential file
|
||||||
|
still work, for a machine that is not a node and for a mesh not yet able to assign anything.
|
||||||
|
`mesh tools --console <url>` goes through a running console with no credential; it is covered by the
|
||||||
|
runtime repository's tests and was not exercised on the live mesh.
|
||||||
|
|
||||||
|
## What shipped bent
|
||||||
|
|
||||||
|
- A module registered by hand from the catalogue with `--source <url> --path modules/<m>` records a URL,
|
||||||
|
not a place on the git seat: `--self` takes the forge path form (`<owner>/<repository>`), which the
|
||||||
|
operator did not pass. The rebuild-on-merge matched the URL anyway.
|
||||||
|
- Modules whose upgrade policy is *record* — the forge among them — answered `tools` only once
|
||||||
|
something pushed their rebuilt runtime; until then they are listed as not answering while still
|
||||||
|
callable. That is the policy doing what it says, not a fault of the console.
|
||||||
@@ -15,12 +15,13 @@ Where the two disagree, the implementation wins and the disagreement is stated.
|
|||||||
| [`04-delivery.md`](04-delivery.md) | Push to running: the three silos, levels, and what a green pipeline proves |
|
| [`04-delivery.md`](04-delivery.md) | Push to running: the three silos, levels, and what a green pipeline proves |
|
||||||
| [`05-runtime-and-installation.md`](05-runtime-and-installation.md) | The node runtime, its modes, and how a node comes into being |
|
| [`05-runtime-and-installation.md`](05-runtime-and-installation.md) | The node runtime, its modes, and how a node comes into being |
|
||||||
| [`06-configuration-and-secrets.md`](06-configuration-and-secrets.md) | Managed files, value resolution, and where secrets live |
|
| [`06-configuration-and-secrets.md`](06-configuration-and-secrets.md) | Managed files, value resolution, and where secrets live |
|
||||||
| [`07-knowledge.md`](07-knowledge.md) | The two knowledge stores, and what each is for |
|
| [`07-knowledge.md`](07-knowledge.md) | The mesh keeps no store: what it knows is what modules answer, and the record is read by one |
|
||||||
| [`08-agents-and-work.md`](08-agents-and-work.md) | Agents as employees, tasks, workflows, and the meeting model |
|
| [`08-agents-and-work.md`](08-agents-and-work.md) | Agents as employees, tasks, workflows, and the meeting model |
|
||||||
| [`09-interfaces-and-observability.md`](09-interfaces-and-observability.md) | How the mesh is reached and watched — tools, board, proxy, health, thoughts |
|
| [`09-interfaces-and-observability.md`](09-interfaces-and-observability.md) | How the mesh is reached and watched — tools, board, proxy, health, thoughts |
|
||||||
| [`10-module-catalogue.md`](10-module-catalogue.md) | The catalogue's shape, and what its shape says |
|
| [`10-module-catalogue.md`](10-module-catalogue.md) | The catalogue's shape, and what its shape says |
|
||||||
| [`11-the-lab.md`](11-the-lab.md) | The lab — the first piece of the new shape that exists, and what it does not yet do |
|
| [`11-the-lab.md`](11-the-lab.md) | The lab — the first piece of the new shape that exists, and what it does not yet do |
|
||||||
| [`12-the-seats.md`](12-the-seats.md) | The seats the mesh defines, who holds one, and where a seat changes resolution |
|
| [`12-the-seats.md`](12-the-seats.md) | The seats the mesh defines, who holds one, and where a seat changes resolution |
|
||||||
|
| [`13-the-console.md`](13-the-console.md) | The mesh's tools on the machine a person sits at, served by a module the mesh assigned there |
|
||||||
|
|
||||||
## What these documents are not
|
## What these documents are not
|
||||||
|
|
||||||
|
|||||||
@@ -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,11 @@
|
|||||||
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/0184-a-service-the-mesh-asked-to-run-is-still-running-a-moment-later.md
|
||||||
|
- 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 +156,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
|
||||||
@@ -466,3 +499,17 @@ run and reported; the exit follows an in-flight apply rather than interrupting i
|
|||||||
not start is rolled back once and the second failure halts; a completed reconcile retires what is older
|
not start is rolled back once and the second failure halts; a completed reconcile retires what is older
|
||||||
than the predecessor and never the predecessor; and the newest of two delivered versions is the one
|
than the predecessor and never the predecessor; and the newest of two delivered versions is the one
|
||||||
that runs.
|
that runs.
|
||||||
|
|
||||||
|
## A service is still running a moment later, 2026-10-02
|
||||||
|
|
||||||
|
[ADR 0184](../../02-DECISIONS/0184-a-service-the-mesh-asked-to-run-is-still-running-a-moment-later.md).
|
||||||
|
The host has always read a unit back after acting on it, because a service manager accepting a
|
||||||
|
command says the transaction was accepted and nothing about the process. The read raced the failure:
|
||||||
|
a daemon that refuses the configuration the mesh just wrote exits a fraction of a second after the
|
||||||
|
manager returns, and one look sees it alive. So the host looks twice, with a pause between, and a
|
||||||
|
unit that was running and is not any more fails its resource by name. A unit still coming up reads
|
||||||
|
as running at both looks and is accepted; a service asked to stop is not waited on.
|
||||||
|
|
||||||
|
No command for this reaches a machine. A module declaring *how to test my configuration* was weighed
|
||||||
|
and refused: the link carries no actions, and a verification command is one. The host is checking
|
||||||
|
the state it was told to establish, which is what it is for. *How it is checked:* ADR 0184's table.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ code:
|
|||||||
- mesh-controller internal/catalogue/build.go
|
- mesh-controller internal/catalogue/build.go
|
||||||
- mesh-controller internal/inventory/secrets.go
|
- mesh-controller internal/inventory/secrets.go
|
||||||
- mesh-controller cmd/mesh-builder
|
- mesh-controller cmd/mesh-builder
|
||||||
updated: 2026-09-12
|
updated: 2026-09-30
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md
|
- 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md
|
||||||
|
- 02-DECISIONS/0037-where-a-module-lives.md
|
||||||
- 02-DECISIONS/0009-modules-and-the-graph.md
|
- 02-DECISIONS/0009-modules-and-the-graph.md
|
||||||
- 02-DECISIONS/0010-delivery.md
|
- 02-DECISIONS/0010-delivery.md
|
||||||
- 02-DECISIONS/0005-the-node-host.md
|
- 02-DECISIONS/0005-the-node-host.md
|
||||||
@@ -60,6 +61,32 @@ module from a repository and a path, and the root-only reading left every existi
|
|||||||
unbuildable — pointed at the catalogue the builder finds no manifest, pointed at a module's source
|
unbuildable — pointed at the catalogue the builder finds no manifest, pointed at a module's source
|
||||||
it finds no manifest either.*
|
it finds no manifest either.*
|
||||||
|
|
||||||
|
## A manifest is checked where it is written
|
||||||
|
|
||||||
|
*Added 2026-09-30, from [issue 148](../../04-ISSUES/148-a-manifest-outside-this-catalogue-has-no-check/00-report.md).*
|
||||||
|
|
||||||
|
The check the mesh applies at registration — the manifest parses strictly, every name in it is a
|
||||||
|
usable one, its routes and events and seats are well formed, and no two manifests given together
|
||||||
|
declare one seat — is a verb on the controller's binary, `module check <manifest>…`, and it needs no
|
||||||
|
mesh. It reads the files it is given, runs the same functions registration runs, prints every problem
|
||||||
|
in the manifest's own words, and exits non-zero if there was one. Somebody describing their own
|
||||||
|
application in their own repository — the case [ADR 0037](../../02-DECISIONS/0037-where-a-module-lives.md)
|
||||||
|
calls the one that matters most — runs it before pushing, and finds out there rather than when a
|
||||||
|
running mesh refuses the registration, or later, when a machine applies something that resolved and
|
||||||
|
should not have.
|
||||||
|
|
||||||
|
**What it cannot know, it says.** A seat another module declares elsewhere is unknown to a check that
|
||||||
|
was not handed that module's manifest, and the output says so rather than refusing: pass the other
|
||||||
|
manifest too. The mesh's own seats it knows from the binary, which is the one place that set may be
|
||||||
|
read without a store ([ADR 0122](../../02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md)
|
||||||
|
keeps the store authoritative, so a claim on a mesh seat is judged fully only at registration, and the
|
||||||
|
check says that too).
|
||||||
|
|
||||||
|
*How it is checked:* the controller's test runs the check over the catalogue checkout beside it and
|
||||||
|
over a manifest with a known fault, and asserts the first passes and the second names the fault; the
|
||||||
|
test that used to be the only check, `TestEveryCatalogueManifestParses`, now stands beside a command
|
||||||
|
anybody can run.
|
||||||
|
|
||||||
## The manifest in the repository is not the manifest the mesh holds
|
## The manifest in the repository is not the manifest the mesh holds
|
||||||
|
|
||||||
A resource names an artifact:
|
A resource names an artifact:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ status: in-progress
|
|||||||
code:
|
code:
|
||||||
- mesh-controller internal/licences
|
- mesh-controller internal/licences
|
||||||
- mesh-controller cmd/mesh-controller/licence.go
|
- mesh-controller cmd/mesh-controller/licence.go
|
||||||
updated: 2026-09-05
|
updated: 2026-10-02
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0024-model-access-is-a-provision.md
|
- 02-DECISIONS/0024-model-access-is-a-provision.md
|
||||||
|
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
|
||||||
- 02-DECISIONS/0009-modules-and-the-graph.md
|
- 02-DECISIONS/0009-modules-and-the-graph.md
|
||||||
- 02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md
|
- 02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md
|
||||||
- 02-DECISIONS/0055-model-access-is-answered-by-a-licence-or-a-node.md
|
- 02-DECISIONS/0055-model-access-is-answered-by-a-licence-or-a-node.md
|
||||||
@@ -96,6 +97,14 @@ So `(node, module)` tells them apart, and asking for a licence per session neede
|
|||||||
identity. Checked rather than argued: two sessions on one machine hold different licences, each is
|
identity. Checked rather than argued: two sessions on one machine hold different licences, each is
|
||||||
given its own key, and releasing one leaves the other.
|
given its own key, and releasing one leaves the other.
|
||||||
|
|
||||||
|
*2026-10-02:* the operator's own agent at a terminal is **not** a consumer of this provision: it is
|
||||||
|
coupled to an Anthropic grant and nothing else, so it uses the `anthropic-licence-manager` seat, whose
|
||||||
|
holder owns the Anthropic licences, their bindings and their rotation, and hands each node's agent its
|
||||||
|
token over the bus
|
||||||
|
([ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md),
|
||||||
|
[36](36-the-operators-agent-on-a-machine.md), [39](39-the-anthropic-licence-manager.md)). This
|
||||||
|
provision stays for the consumers that do not care which vendor answers.
|
||||||
|
|
||||||
**What is still open is the rest of the gap, and it is the harder half.** A *worker* is not one
|
**What is still open is the rest of the gap, and it is the harder half.** A *worker* is not one
|
||||||
per machine — many can run on one, from one module — so `(node, module)` cannot name them apart
|
per machine — many can run on one, from one module — so `(node, module)` cannot name them apart
|
||||||
and this reasoning does not extend to them. That belongs with
|
and this reasoning does not extend to them. That belongs with
|
||||||
|
|||||||
@@ -148,6 +148,10 @@ than reproduced from a declaration — because there is nothing to reproduce it
|
|||||||
([ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md)), not by holding a
|
([ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md)), not by holding a
|
||||||
copy. It is that reader; there is not a second agent for it.
|
copy. It is that reader; there is not a second agent for it.
|
||||||
|
|
||||||
|
*2026-09-30:* the reading is a module's — `records`, [35 — Reading the record](35-reading-the-record.md),
|
||||||
|
[ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md). The
|
||||||
|
session, when built, asks it rather than reading for itself; what it adds is judgement, not text.
|
||||||
|
|
||||||
**It answers into a symptom search**, so what it knows appears beside ordinary results rather than
|
**It answers into a symptom search**, so what it knows appears beside ordinary results rather than
|
||||||
only when it is asked. **And when it cannot be reached, the search says so.** A result set that
|
only when it is asked. **And when it cannot be reached, the search says so.** A result set that
|
||||||
silently omits this material looks identical to one where nothing matched — the same rule as the
|
silently omits this material looks identical to one where nothing matched — the same rule as the
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -361,6 +380,13 @@ bridged. It is three things:
|
|||||||
|
|
||||||
Nothing is built of this before §10's bed passes; the MCP surface is a thin adapter over (2).
|
Nothing is built of this before §10's bed passes; the MCP surface is a thin adapter over (2).
|
||||||
|
|
||||||
|
*Built, and then made a module — 2026-09-30.* (1) and (2) exist: `operator issue` and the `mesh`
|
||||||
|
client. What (2) describes as a program on the workstation is now the recovery path; the surface an
|
||||||
|
operator uses is a module the mesh assigns to the machine, holding a credential the mesh minted —
|
||||||
|
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
|
||||||
|
[34 — The console](34-the-console.md). The tool list it asks for is no longer
|
||||||
|
`catalog_tools`, which nothing served: each runtime answers `tools` for its own module.
|
||||||
|
|
||||||
## 8. What a module sees, and what the wire does
|
## 8. What a module sees, and what the wire does
|
||||||
|
|
||||||
**The contract a module is written against does not change.** `publish` on an envelope becomes a
|
**The contract a module is written against does not change.** `publish` on an envelope becomes a
|
||||||
|
|||||||
@@ -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,9 +1,14 @@
|
|||||||
---
|
---
|
||||||
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/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
|
||||||
|
- 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
|
||||||
- 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
|
||||||
- 02-DECISIONS/0051-shared-data-is-the-operators.md
|
- 02-DECISIONS/0051-shared-data-is-the-operators.md
|
||||||
@@ -14,10 +19,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 +33,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 +71,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 +114,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 +135,53 @@ 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.
|
||||||
|
|
||||||
|
*2026-10-02:* two of them are written. [ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)
|
||||||
|
records the account as a node fact and the home as a placement root, reconstructed from what shipped;
|
||||||
|
[ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
||||||
|
generalises §3's boundary to every directory under a home. The first member of the §2 family is
|
||||||
|
designed in [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md). User-scoped
|
||||||
|
units are [ADR 0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md),
|
||||||
|
written the same day; still unwritten: several accounts per node, and the CA. On the same day every node of the
|
||||||
|
live mesh still carried an empty account.
|
||||||
|
|
||||||
## 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 +190,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 +209,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 +233,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
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: proposed
|
status: in-progress
|
||||||
code: []
|
code:
|
||||||
updated: 2026-09-27
|
- mesh-controller: internal/catalogue/jails_into.go, internal/catalogue/manifest.go (Jail, Jailing)
|
||||||
|
- mesh-catalog: modules/fail2ban (jailing, the base and the seat's verbs), modules/mailu, modules/route-proxy, modules/gitea (jails)
|
||||||
|
- mesh-host: internal/declaration/declaration.go (a container's logging)
|
||||||
|
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/0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md
|
||||||
|
- 02-DECISIONS/0186-a-ban-list-never-holds-a-neighbour.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 31 — A module declares its fail2ban jail, and the mesh composes them per node
|
# 31 — A module declares its fail2ban jail, and the mesh composes them per node
|
||||||
@@ -63,3 +68,38 @@ jail, composed from the postgres module's manifest, without anyone editing a nod
|
|||||||
beside)
|
beside)
|
||||||
- mesh-catalog `modules/fail2ban` (the base: sshd, recidive, ignoreip); the service modules
|
- mesh-catalog `modules/fail2ban` (the base: sshd, recidive, ignoreip); the service modules
|
||||||
(`postgres`, `mssql`, `mailu`) that will declare jails
|
(`postgres`, `mssql`, `mailu`) that will declare jails
|
||||||
|
|
||||||
|
## Decided and built, 2026-10-02
|
||||||
|
|
||||||
|
[ADR 0179](../../02-DECISIONS/0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md)
|
||||||
|
made this the rule and built it. A module declares `jails` — each a name, the `failregex` of a
|
||||||
|
failed attempt in its log, and the stanza's own keys — and the fail2ban module declares `jailing`:
|
||||||
|
the one file the stanzas compose into and the directory each filter lands in. The controller gathers
|
||||||
|
every assigned module's jails per node into those; the holder's daemon restarts on the composed file.
|
||||||
|
|
||||||
|
What made it workable was the log. A container's output went to a file of the runtime's own, under
|
||||||
|
a path that changes when the container is recreated, so no jail could read a container's service
|
||||||
|
however it logged. A container now declares `logging: journald`, the host runs it with the journal as
|
||||||
|
its driver, and a jail reads it with `backend = systemd` and a `journalmatch` on the container's
|
||||||
|
name — the same way the base's ssh jail has always read the ssh daemon. The first three doors: the
|
||||||
|
mail front end (every login failure on its proxying ports), the forge (a failed authentication
|
||||||
|
attempt) and the public proxy (a certificate or request for a name the mesh does not serve, which
|
||||||
|
the proxy now says in its log). The base is strict — three in a day for a day; twice banned in two
|
||||||
|
weeks for four — and the mesh's own range stays never banned.
|
||||||
|
|
||||||
|
The seat the module holds serves `status`, `banned`, `ban` and `unban`, from a runtime that carries
|
||||||
|
only the fail2ban client with the daemon's socket shared in; the jails are composed, the ban list is
|
||||||
|
the daemon's, and both are read through the console.
|
||||||
|
|
||||||
|
*How it is checked:* ADR 0179's table.
|
||||||
|
|
||||||
|
## What the first jails taught, 2026-10-02
|
||||||
|
|
||||||
|
[ADR 0186](../../02-DECISIONS/0186-a-ban-list-never-holds-a-neighbour.md). Within an hour of the
|
||||||
|
first public jail the home server had banned the house's own router: the router reflects local
|
||||||
|
traffic, so every client in the building arrives as the gateway's address. The never-ban list now
|
||||||
|
holds every private range as well as the mesh's own. And the mesh read its own ban chain as a
|
||||||
|
foreign rule set on that machine, because the chain hangs off the container runtime's user chain and
|
||||||
|
that machine's forward policy is the runtime's DROP — the reader now calls a chain of source-named
|
||||||
|
refusals a ban wherever it hangs, as it already did for the packet filter's own tables.
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ code:
|
|||||||
- mesh-host internal/apply/apply.go
|
- mesh-host internal/apply/apply.go
|
||||||
- mesh-tools src/main.ts
|
- mesh-tools src/main.ts
|
||||||
- mesh-catalog modules/mesh-catalog
|
- mesh-catalog modules/mesh-catalog
|
||||||
updated: 2026-09-28
|
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/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
|
||||||
@@ -23,6 +24,7 @@ decisions:
|
|||||||
- 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
|
- 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
|
||||||
- 02-DECISIONS/0135-a-module-version-prepares-its-state-before-it-runs.md
|
- 02-DECISIONS/0135-a-module-version-prepares-its-state-before-it-runs.md
|
||||||
- 02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md
|
- 02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md
|
||||||
|
- 02-DECISIONS/0187-a-dead-tracker-is-not-the-machines-failure.md
|
||||||
- 02-DECISIONS/0134-the-mesh-says-what-it-applied.md
|
- 02-DECISIONS/0134-the-mesh-says-what-it-applied.md
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -120,6 +122,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,
|
||||||
@@ -463,6 +471,21 @@ moment the mesh can mint for itself: **the bus's own accounts** (§the bootstrap
|
|||||||
needs an account before it can run) and **the vault's own credential**. Any third exception is a
|
needs an account before it can run) and **the vault's own credential**. Any third exception is a
|
||||||
design failure, and naming these two is what makes a third one visible.
|
design failure, and naming these two is what makes a third one visible.
|
||||||
|
|
||||||
|
## What a step is answerable for, 2026-10-02
|
||||||
|
|
||||||
|
[ADR 0187](../../02-DECISIONS/0187-a-dead-tracker-is-not-the-machines-failure.md). A step gates its
|
||||||
|
module and not the machine ([ADR 0136](../../02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md)),
|
||||||
|
but a step that exits non-zero still leaves the machine reporting that it is not doing what it was
|
||||||
|
told — and a clean apply gates other things entirely, the found firewall's retirement among them. So
|
||||||
|
what a step calls a failure matters beyond the step.
|
||||||
|
|
||||||
|
The rule the media step now follows, and the one to copy: a step fails for what the mesh chose and
|
||||||
|
can fix, and reports what it merely found and cannot. An indexer entry the mesh re-pointed at this
|
||||||
|
mesh's proxy is plumbing the mesh is answerable for; whether the public tracker behind it answers
|
||||||
|
today is not. An entry the operator listed is the operator's to insist on, and still fails. Six
|
||||||
|
hours of a machine reading as broken, for one tracker that had died, is what the distinction costs
|
||||||
|
when it is missing.
|
||||||
|
|
||||||
## 11. Open
|
## 11. Open
|
||||||
|
|
||||||
**Semantic change has no mechanical defence** (§8). Recorded as open rather than solved, because
|
**Semantic change has no mechanical defence** (§8). Recorded as open rather than solved, because
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: designed
|
status: implemented
|
||||||
code: []
|
code: [mesh-controller, mesh-tools]
|
||||||
updated: 2026-09-28
|
updated: 2026-10-02
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md
|
||||||
|
- 02-DECISIONS/0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.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/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
|
||||||
- 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
|
||||||
@@ -78,6 +83,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
|
||||||
@@ -102,6 +117,14 @@ being something a person carries and becomes something the mesh runs, on a node,
|
|||||||
An agent's authority can then be role-shaped: *the forge's tools*, rather than a list of
|
An agent's authority can then be role-shaped: *the forge's tools*, rather than a list of
|
||||||
module-specific names that changes the day the forge is replaced.
|
module-specific names that changes the day the forge is replaced.
|
||||||
|
|
||||||
|
*Decided and designed on 2026-09-30:* the module is the console —
|
||||||
|
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
|
||||||
|
[34 — The console](34-the-console.md). It builds the second half of §5 now (a module's tools are
|
||||||
|
asked of the module, through a `tools` verb every runtime answers) and lists a role's tools when the
|
||||||
|
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
|
||||||
@@ -120,6 +143,60 @@ by side until nothing is bound to the old one.
|
|||||||
- **Two nodes holding one node-scoped seat derive two addresses.** Checked by the same test as the rest
|
- **Two nodes holding one node-scoped seat derive two addresses.** Checked by the same test as the rest
|
||||||
of the subject table.
|
of the subject table.
|
||||||
|
|
||||||
|
## What is built, 2026-09-30
|
||||||
|
|
||||||
|
Under [ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md): §2's
|
||||||
|
two constraints (the protocol in the store's row, seeded additively; a verb with description and
|
||||||
|
schema, a bare name still accepted), §3 for the mesh's seats (holding refused by naming the missing
|
||||||
|
verbs), §4 (a node-scoped seat's tool carries the node as its last subject token; a user publishes
|
||||||
|
`*`), and the third family — twelve verbs on the `mesh-controller` seat, each running the command it
|
||||||
|
names in the controller's own binary. §5's first half is served rather than read: the seat's `tools`
|
||||||
|
verb answers every seat's tools from the records, because the console cannot read the store; 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## The intrusion seat's verbs, 2026-10-02
|
||||||
|
|
||||||
|
[ADR 0179](../../02-DECISIONS/0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md).
|
||||||
|
The second node-scoped seat to carry verbs: `node-intrusion-prevention` serves `status` (every jail
|
||||||
|
with what it watches and holds), `banned` (every address held now, with its jail and when the ban
|
||||||
|
ends), `ban` and `unban` (an operator's act on the live ban list). The fail2ban module serves them
|
||||||
|
from a runtime that carries only the daemon's client, the socket shared in from the machine — no
|
||||||
|
capability, no machine network, since the daemon on the machine does the banning. That runtime is the
|
||||||
|
per-module container [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)
|
||||||
|
retires; the verbs and the client are the same code once the node's own runtime loads them as a bundle.
|
||||||
|
The module's own tool beside them reads one jail's effective settings. *How it is checked:* ADR 0179'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
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
---
|
||||||
|
layer: to-be
|
||||||
|
status: implemented
|
||||||
|
code: [mesh-catalog, mesh-tools, mesh-controller]
|
||||||
|
updated: 2026-10-02
|
||||||
|
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/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/0035-one-implementation-several-surfaces.md
|
||||||
|
- 02-DECISIONS/0034-the-local-account-owns-the-mesh.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 34 — The console
|
||||||
|
|
||||||
|
**The mesh's tools, on the machine a person sits at, served by a module the mesh assigned there.**
|
||||||
|
An agent reaches them over MCP on the machine's loopback; a person reaches the same endpoint. Nothing is
|
||||||
|
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)).
|
||||||
|
|
||||||
|
> **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
|
||||||
|
|
||||||
|
A module, `mesh-console`, in the catalogue. Its image is the tool runtime's own — the client that
|
||||||
|
already speaks the bus as a command line and as an MCP server — started in a mode that reads the
|
||||||
|
module's credential and listens on loopback. It has no state, no provision, no seat. What it needs is
|
||||||
|
the bus, which it gets the way every module does: a credential the mesh minted for `<node>.mesh-console`,
|
||||||
|
sealed to the machine, delivered as the module's own secret.
|
||||||
|
|
||||||
|
*2026-10-02:* it gains one provision, at node scope — the MCP endpoint on loopback, serving the port
|
||||||
|
the machine gave it — so that a module whose software must be told where the console is requires that
|
||||||
|
and is coupled to an endpoint rather than to a module's name
|
||||||
|
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)). The first
|
||||||
|
consumer is the operator's agent, [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md) §6;
|
||||||
|
a machine without the console refuses such a module by name. Nothing else above changes: no seat, no
|
||||||
|
state, no tools of its own.
|
||||||
|
|
||||||
|
Its manifest says three things nothing else in the catalogue says together:
|
||||||
|
|
||||||
|
- `invokes: ["*"]` — it calls every tool on the mesh, and the bus grants exactly that publish side;
|
||||||
|
- `listens` on a port `from: machine` — the filter opens nothing for it, because loopback is not outside;
|
||||||
|
- no `emits`, no `consumes`, no `tools` — it answers nothing on the bus and nobody can address it there.
|
||||||
|
|
||||||
|
## 2. What it serves, and to whom
|
||||||
|
|
||||||
|
**One endpoint, `POST /mcp` on the machine's loopback**, speaking MCP over HTTP: `initialize`,
|
||||||
|
`tools/list`, `tools/call`. An agent on the machine is pointed at it once — the address is the machine's
|
||||||
|
own and never changes — and sees every tool the mesh can say it has. A person at a terminal uses the
|
||||||
|
same endpoint through the `mesh` client, or through anything that can make an HTTP request; the client
|
||||||
|
needs no credential, because the console holds it.
|
||||||
|
|
||||||
|
**The endpoint is the machine's login.** It binds `127.0.0.1` and nothing else. Whoever can connect is
|
||||||
|
on the machine, and whoever is on the machine is the account that owns the mesh there
|
||||||
|
([ADR 0034](../../02-DECISIONS/0034-the-local-account-owns-the-mesh.md),
|
||||||
|
[ADR 0144](../../02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md)). There is no
|
||||||
|
token, no login page and no second identity, on purpose: a credential a person had to carry to reach
|
||||||
|
their own machine's console would be the arrangement this replaces, moved one hop.
|
||||||
|
|
||||||
|
## 3. How it knows what the mesh can do
|
||||||
|
|
||||||
|
Design [33](33-the-tools-the-mesh-answers.md) §5 splits discovery in two: a role's tools are read from
|
||||||
|
the mesh's records, a module's own are asked of the module. The console builds the second half now and
|
||||||
|
reads the first when it exists.
|
||||||
|
|
||||||
|
**Every tool runtime answers `tools`.** The runtime that serves a module's tools also serves one verb of
|
||||||
|
its own under that module's name, `mesh.mod.<module>.tool.tools`, answering the module's tool names,
|
||||||
|
descriptions and argument schemas — the definitions from the code that answers them, and from nowhere
|
||||||
|
else. A module may not name a tool of its own `tools`; the runtime refuses the collision at load.
|
||||||
|
|
||||||
|
**The console asks the catalogue which modules the mesh holds, then asks each.** `catalog_modules`
|
||||||
|
answers the roster; one `tools` request per module, in parallel, answers the list. The bus refuses at
|
||||||
|
once a request nothing serves, so a module that is not running costs nothing and is named in the answer
|
||||||
|
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.
|
||||||
|
|
||||||
|
**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
|
||||||
|
that knows a tool's name asks for it by `<module>.<tool>` and the module answers or the bus says why not.
|
||||||
|
|
||||||
|
**What is missing from the list, and until when.** A role's tools and the mesh's own verbs — `status`,
|
||||||
|
`push`, `assign` — are the `mesh-controller` seat's under ADR 0132 and are not served yet; their three
|
||||||
|
prerequisites are listed in that record. When the seat serves them, the console lists them beside the
|
||||||
|
modules' own, and the person stops opening a shell for the mesh's own questions. Until then the console
|
||||||
|
says so in its handshake.
|
||||||
|
|
||||||
|
## 4. Where it runs
|
||||||
|
|
||||||
|
On whichever machines an operator sits at, by assignment. It is not on the control node by default and
|
||||||
|
does not need to be: it reaches the bus like any module, from anywhere in the mesh. A machine that is
|
||||||
|
not a node cannot have it, which is the right refusal — the mesh reaches what it declares, and a
|
||||||
|
workstation that wants the console joins first.
|
||||||
|
|
||||||
|
The person's credential and the `mesh` client (design [25](25-the-bus-on-nats.md) §7) remain the path
|
||||||
|
for a machine that is not a node, and the path to a mesh not yet far enough along to assign anything.
|
||||||
|
|
||||||
|
## 5. Removing it
|
||||||
|
|
||||||
|
Unassigning the console from a machine revokes its bus account at the next composition and stops the
|
||||||
|
container; nothing is left on the machine that could still connect. An agent pointed at the loopback
|
||||||
|
address gets a refused connection, which is the truthful answer.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Check | Defends |
|
||||||
|
|---|---|
|
||||||
|
| a module invoking one tool may publish that subject and no other tool's; `*` may publish every one; neither may publish an event or subscribe what it did not consume | ADR 0152, the grant |
|
||||||
|
| a module registering two tools answers three names to `tools`, with schemas; a module naming its own `tools` is refused at load | ADR 0152, discovery |
|
||||||
|
| against a real bus: two modules up, a third held and not running — the console lists the two and names the third as not answering | ADR 0152, silence is not success |
|
||||||
|
| a call through the console's endpoint reaches a module over the bus and the answer is the module's own, unshaped | ADR 0035, a surface decides nothing |
|
||||||
|
| on the live mesh: the console assigned to a workstation answers `tools/list` on loopback and a call to the forge returns repositories | the exit of work-order step 3 |
|
||||||
|
| the composed filter for a machine carrying the console opens no port for it | ADR 0144 |
|
||||||
|
|
||||||
|
## What shipped, 2026-09-30
|
||||||
|
|
||||||
|
Everything above, the same day: mesh-controller PR 164 (`invokes`, `module check`), mesh-tools PR 20
|
||||||
|
(`mesh serve`, the `tools` verb), mesh-catalog PR 181 (`mesh-console`). Verified on the live mesh: the
|
||||||
|
console assigned to a workstation answered `tools/list` on its loopback with 62 tools from the modules
|
||||||
|
whose runtimes had been rebuilt to answer `tools`, named 36 modules as not answering (modules that serve
|
||||||
|
no tools, and modules whose new runtime the mesh records rather than rolls out), and a `tools/call` of
|
||||||
|
the forge's `gitea_list_repos` returned repositories. An agent on that machine reaches it as an HTTP
|
||||||
|
MCP server and reports it connected. The mesh assigned the declared port unchanged, which is what a
|
||||||
|
machine with nothing else on it does; the console binds whatever it is given.
|
||||||
|
|
||||||
|
Two things shipped bent, both stated in [`00-as-is/13-the-console.md`](../00-as-is/13-the-console.md):
|
||||||
|
the person's client through the console (`--console`) exists and was exercised in the test suite, not
|
||||||
|
on the live mesh; and a module registered from the catalogue by hand recorded its source as a URL rather
|
||||||
|
than as a path on the git seat, because `--self` takes the forge path form — the rebuild-on-merge still
|
||||||
|
matched it by URL.
|
||||||
|
|
||||||
|
## What this does not settle
|
||||||
|
|
||||||
|
- Narrowing a console's grant per assignment. ADR 0046 makes it a setting; nothing reads one yet.
|
||||||
|
- The mesh's own verbs on the bus. Design 33's third family; this document only says where they appear
|
||||||
|
once they exist.
|
||||||
|
- A person's identity behind the console. The mesh sees the console's account; design 15 keeps the
|
||||||
|
question open.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md) — the decision
|
||||||
|
- [33 — The tools the mesh answers](33-the-tools-the-mesh-answers.md) — what the console lists
|
||||||
|
- [25 — The bus on NATS](25-the-bus-on-nats.md) §7 — the person's client this makes a module of
|
||||||
|
- [issue 147](../../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md) — the symptom
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
layer: to-be
|
||||||
|
status: implemented
|
||||||
|
code: [mesh-catalog modules/records]
|
||||||
|
updated: 2026-09-30
|
||||||
|
decisions:
|
||||||
|
- 02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md
|
||||||
|
- 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
|
||||||
|
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 35 — Reading the record
|
||||||
|
|
||||||
|
**The design record, answered from a checkout the mesh keeps, at the commit it read.** A module,
|
||||||
|
`records`, holds a working copy of a repository of markdown — this one, for this mesh — and answers
|
||||||
|
where a phrase appears, what a document says, what a folder holds and where the copy stands. The
|
||||||
|
console lists those answers beside every other tool
|
||||||
|
([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)).
|
||||||
|
|
||||||
|
## 1. What it keeps, and why that is not a copy
|
||||||
|
|
||||||
|
A git checkout, cloned from the forge that holds the `git` seat, brought up to date on every merge the
|
||||||
|
forge announces and every ten minutes besides. The bytes are the repository's; nothing is derived
|
||||||
|
from them and stored. What [ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md)
|
||||||
|
refused was a second store that is *searched* while the first is *edited*, drifting silently. A
|
||||||
|
checkout cannot drift; it can lag, and the lag is in every answer as the commit it was read at and
|
||||||
|
in `records_status` as when it was last brought up to date.
|
||||||
|
|
||||||
|
The checkout is reset to the origin on every sync, never merged: it is the mesh's, so a local change
|
||||||
|
is nobody's.
|
||||||
|
|
||||||
|
## 2. What it answers
|
||||||
|
|
||||||
|
| tool | answers |
|
||||||
|
|---|---|
|
||||||
|
| `records_search` | every place a phrase appears, as written and case-insensitively: document, line, the nearest heading above it; bounded, and says when it was |
|
||||||
|
| `records_read` | one document, whole, or its first part with a note when very long |
|
||||||
|
| `records_list` | what a folder holds: sub-folders and documents |
|
||||||
|
| `records_status` | repository, forge, commit and its date, last sync, document count, last error |
|
||||||
|
| `records_sync` | bring the checkout up to date now |
|
||||||
|
|
||||||
|
No ranking and no summary, on purpose: a record is found by its own words, and the reasoning is in
|
||||||
|
the document, not in the tool.
|
||||||
|
|
||||||
|
## 3. What it is told, and what it refuses to guess
|
||||||
|
|
||||||
|
Three things, none from a manifest ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md)):
|
||||||
|
the directory its checkout lives in (a resource the mesh gives it), the forge's address (the `git`
|
||||||
|
provision's binding, written into a file as `scheme://host:port`), and the repository's path on the
|
||||||
|
forge (a **setting**, `{"repository": "<owner>/<name>"}`). Without the third it serves no tools and its
|
||||||
|
log says so. Public repositories only; it holds no credential.
|
||||||
|
|
||||||
|
## 4. How it is found
|
||||||
|
|
||||||
|
The console asks every module what it serves and lists `records_search` with a description that says
|
||||||
|
when to call it — *search the literal words of a symptom or a term before forming a hypothesis*. That
|
||||||
|
is 0025's second half in today's mesh: there is no store to be beside, and an agent choosing from a
|
||||||
|
tool list is the search.
|
||||||
|
|
||||||
|
## 5. Where it runs
|
||||||
|
|
||||||
|
Anywhere a node has the forge in reach; one assignment is enough, and a second on another machine is
|
||||||
|
harmless. The mesh session of design 15, when it exists, calls this rather than reading for itself.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Check | Defends |
|
||||||
|
|---|---|
|
||||||
|
| a phrase in one document of a repository the test makes comes back from that document, with the commit; a second commit on the origin is pulled and the next answer names it | ADR 0025's check, ADR 0153 |
|
||||||
|
| a path outside the checkout is refused; an empty search is refused | the reader reads the repository and nothing else |
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- Ranking or meaning. A search that understands a question is the session's job, not the reader's.
|
||||||
|
- A private repository. That is a credential the module would have to hold, and a decision about
|
||||||
|
what may read what.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)
|
||||||
|
- [ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md)
|
||||||
|
- [34 — The console](34-the-console.md) — what lists it
|
||||||
|
- [issue 006](../../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md)
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
---
|
||||||
|
layer: to-be
|
||||||
|
status: designed
|
||||||
|
code: []
|
||||||
|
updated: 2026-10-02
|
||||||
|
decisions:
|
||||||
|
- 02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
|
||||||
|
- 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
|
||||||
|
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.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/0152-the-operators-surface-is-a-module-the-console.md
|
||||||
|
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
|
||||||
|
- 02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md
|
||||||
|
- 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 36 — The operator's agent on a machine: the `claude-code` module
|
||||||
|
|
||||||
|
**The agent a person runs at a terminal, put on the machine by the mesh, instructed by the mesh, pointed
|
||||||
|
at the console, and holding the licence the manager hands it.** It is a member of the family
|
||||||
|
[to-be 29 §2](29-a-node-has-operator-accounts.md) names, the modules that touch a person's machine, and
|
||||||
|
its counterpart is [39 — The Anthropic licence manager](39-the-anthropic-licence-manager.md).
|
||||||
|
|
||||||
|
What it replaces: the predecessor's module of the same name and a sibling, which placed six files under
|
||||||
|
the operator's home. The predecessor is retired; the six files are still on both workstations telling
|
||||||
|
every session to use tools that no longer exist.
|
||||||
|
|
||||||
|
**Three rules shape everything below.** The host is module-agnostic: it installs the package and gives
|
||||||
|
the module a state directory, and knows no vendor, no agent, no path under a home. The controller has no
|
||||||
|
part beyond resolving what it resolves for every module. And the module handles its own files: the
|
||||||
|
mesh's part of the agent's configuration is written by the module's own code, from what the mesh
|
||||||
|
delivered it and what the manager handed it.
|
||||||
|
|
||||||
|
## 1. Where the mesh's configuration lives: the agent's managed directory, not the home
|
||||||
|
|
||||||
|
The agent reads a machine-wide, administrator-owned configuration directory under `/etc`, documented
|
||||||
|
by the vendor: a managed settings file that outranks every user and project setting; a key in it that
|
||||||
|
adds HTTP tool servers *beside* a person's own without blocking them; and a managed instruction file every
|
||||||
|
session reads before the user's and the project's. The agent has **no** machine-wide directory for
|
||||||
|
rules, skills, slash commands or hooks; those exist only under a home or a project.
|
||||||
|
|
||||||
|
So the mesh's part of the agent's configuration lives there, **owned whole by the module**, and the home
|
||||||
|
is left alone. What the predecessor shipped as two rule files and two skills folds into the managed
|
||||||
|
instruction file and the manager's tools:
|
||||||
|
|
||||||
|
| the predecessor placed | becomes |
|
||||||
|
|---|---|
|
||||||
|
| `~/.claude/CLAUDE.md` | the managed instruction file: how a session on this mesh works (§3) |
|
||||||
|
| `~/.claude/rules/00-hal-mesh.md`, `~/.claude/rules/conventions.md` | sections of the same file: this node's identity, the repositories' conventions |
|
||||||
|
| `~/.claude/settings.json`, merged | the managed settings file: the mesh's keys only, outranking nothing a person did not also set |
|
||||||
|
| `~/.claude/skills/hal-switch-license/SKILL.md` | the manager seat's `switch` verb, listed by the console, and a sentence in the instruction file saying to use it |
|
||||||
|
| `~/.claude/skills/cleanup/SKILL.md` | nothing; it named the predecessor's forge |
|
||||||
|
| the console's entry in the agent's user-scope state | the managed settings' tool-server key, from the console's provision (§4) |
|
||||||
|
|
||||||
|
**The home.** Under [ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
||||||
|
every path under `~/.claude` is *found*, with one exception: the agent's credentials file, which the
|
||||||
|
module's own code writes for a subscription licence (§5). The person's memory, history, projects, local
|
||||||
|
settings, their own rules, skills and tool servers are never read or written by the mesh. **The six
|
||||||
|
predecessor files are the operator's to remove, once, on each workstation**; the module's documentation
|
||||||
|
lists them, and until they go the agent reads stale instructions beside the mesh's.
|
||||||
|
|
||||||
|
## 2. What the module declares and what its code writes
|
||||||
|
|
||||||
|
**Declared, applied by the host:** the agent's package (§7); the module's state directory; a facts file
|
||||||
|
in that directory carrying the node's name, the operator account, the console's endpoint, the module's
|
||||||
|
settings; the bus, the console's provision, and that it uses the `anthropic-licence-manager` seat.
|
||||||
|
Nothing under the home, nothing under `/etc`.
|
||||||
|
|
||||||
|
**Written by the module's code**, from the facts file and the manager's hand-over, whenever either
|
||||||
|
changes:
|
||||||
|
|
||||||
|
| path | content |
|
||||||
|
|---|---|
|
||||||
|
| the managed settings file | the mesh's keys: the tool servers (the console, plus any the operator declared as settings), the attribution trailers, and — for an API-key binding only — the key-helper that serves the key |
|
||||||
|
| the managed instruction file | §3 |
|
||||||
|
| the agent's credentials file under the operator's home | for a subscription binding only: the access token the manager handed over, as the operator, readable by the operator alone, atomic, no refresh token |
|
||||||
|
| the module's keypair in its state | made once, the private half never leaves (§5) |
|
||||||
|
|
||||||
|
Writing under `/etc` and as the operator under the home are two escalations the module's code performs
|
||||||
|
for itself; the mesh does not run the module as root for everyone, and the caller does not know
|
||||||
|
([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
|
||||||
|
|
||||||
|
**Which settings keys are the mesh's.** A key is the mesh's when it encodes a rule of the mesh: the tool
|
||||||
|
servers that reach the mesh, the attribution convention of its repositories, the key-helper a binding
|
||||||
|
requires. The model, the spinner, the drafts and every other preference are the person's, and the
|
||||||
|
predecessor's experience with the model key is the evidence: a mesh that sets a preference reverts a
|
||||||
|
person's choice on every push.
|
||||||
|
|
||||||
|
## 3. What the instruction file says
|
||||||
|
|
||||||
|
Prose, not a paste; the file is the module's.
|
||||||
|
|
||||||
|
**How a session on this mesh works.** The console is the only path to the mesh, and its tools are the
|
||||||
|
vocabulary: the record is asked through the records module, symptom first — the literal error text before
|
||||||
|
a hypothesis ([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md));
|
||||||
|
the mesh is asked and changed through the controller seat's verbs; the forge through the forge module's
|
||||||
|
tools; a licence through the `anthropic-licence-manager` seat's verbs, never by editing a file. The hard
|
||||||
|
rules in new words: a file the mesh manages is changed through the verb that owns it or through the
|
||||||
|
catalogue, never on disk; a store's database is never written by hand; main is never pushed; the mesh
|
||||||
|
creates no symlinks and nobody else does; a package is declared, not installed by hand. The glossary's
|
||||||
|
words, none of the predecessor's.
|
||||||
|
|
||||||
|
**Who this node is.** The node's name, from the facts file; the node's role, from the module's settings
|
||||||
|
on the node's layer; and that the other nodes are asked of the controller's `nodes` verb rather than
|
||||||
|
listed here, because a table is a copy that drifts.
|
||||||
|
|
||||||
|
**The repositories' conventions.** Concise commit messages in the imperative, about why; a branch, a
|
||||||
|
pull request and a human approval for every merge; test before pushing, because nodes update unattended;
|
||||||
|
the playbooks in the record.
|
||||||
|
|
||||||
|
## 4. The console
|
||||||
|
|
||||||
|
The module tells the agent where the console is, and the port is the console's to say. **The console
|
||||||
|
provides a node-scoped provision** — its MCP endpoint on loopback — serving the port the machine gave
|
||||||
|
it, and the module requires it. A requirement names what the consumer is coupled to
|
||||||
|
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)); co-location
|
||||||
|
resolves it; a machine without the console refuses the module by name. [To-be 34](34-the-console.md) is
|
||||||
|
amended in the same change; issue 192 (open) found the gap.
|
||||||
|
|
||||||
|
**Other tool servers** a person wants on every machine, or on one, are a declared setting of this module
|
||||||
|
— mesh layer or node layer — rendered into the same managed key. A module tool, `mcp_configure`,
|
||||||
|
validates a server and sets the setting through the controller's settings verb, so the list stays
|
||||||
|
declared state. The agent's own HTTP-only constraint for managed servers applies; a person's local
|
||||||
|
command-based servers stay their own, in their own file.
|
||||||
|
|
||||||
|
**The entry's name is `mesh`.** The hand-made entry both workstations carry today is named after this
|
||||||
|
installation, which a definition may not be ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md));
|
||||||
|
it is the person's to remove, and until then the agent sees the mesh's tools twice.
|
||||||
|
|
||||||
|
## 5. The licence: the consumer side
|
||||||
|
|
||||||
|
[ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
|
||||||
|
decides it; to-be 39 is the manager's half. This module:
|
||||||
|
|
||||||
|
- **makes a keypair** in its state the first time it runs and registers the public half with the seat;
|
||||||
|
- **serves `apply`**: the manager's hand-over, a token sealed to the module's key, with the licence's
|
||||||
|
name and kind. A rotation of the same licence is applied only if newer within one lineage; a switch is
|
||||||
|
applied regardless, because across licences the expiries are unrelated. The answer says applied or
|
||||||
|
refused and why, and never echoes a token;
|
||||||
|
- **pulls** at start and when its token nears expiry, by the seat's `current` verb, and keeps the last
|
||||||
|
token when the manager does not answer, saying so;
|
||||||
|
- **writes** for a subscription licence the credentials file as the operator, access-token-only; for the
|
||||||
|
API-key licence sets the key-helper in the managed settings to a small program that prints the key
|
||||||
|
from the module's state, so no file under the home is touched;
|
||||||
|
- **offers a login to the manager**: when the credentials file changes by a person's login, it reads the
|
||||||
|
account's identity from the agent's state file and offers the grant to the seat, sealed to the manager's
|
||||||
|
key, for adoption; the manager decides;
|
||||||
|
- **serves `licence_status`**: which licence and kind this node holds, when the token expires, whether
|
||||||
|
the file matches what was handed over — by fingerprint, never by value.
|
||||||
|
|
||||||
|
Switching is the seat's `switch` verb, asked through the console; this module only applies what it is
|
||||||
|
handed.
|
||||||
|
|
||||||
|
## 6. Scope, settings and the order of assignment
|
||||||
|
|
||||||
|
**Every node with an operator account** ([ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)).
|
||||||
|
None has one today; the operator states them first. **Per node:** the role. **Per mesh or per node:**
|
||||||
|
extra tool servers. **Prerequisite:** the manager holds its seat and has adopted the licences.
|
||||||
|
|
||||||
|
**Order:** the manager assigned and a refresh observed; the console's provision in the catalogue; this
|
||||||
|
module on one workstation; the six predecessor files and the hand-made console entry removed there; a
|
||||||
|
new session read to confirm it sees the mesh's instruction file, the console's tools under `mesh`, and
|
||||||
|
its licence; then the rest.
|
||||||
|
|
||||||
|
## 7. The package
|
||||||
|
|
||||||
|
The module declares the agent's package. The distribution every node runs does not carry it in its
|
||||||
|
repositories: the two workstations have it from a build the predecessor's helper made from the community
|
||||||
|
repository, and nothing updates it since. On those two the declaration is satisfied. **On a fresh machine
|
||||||
|
the host's package manager refuses it, in its own words, and the module is not applied there.** The
|
||||||
|
answer is a package repository for this ecosystem as a seat
|
||||||
|
([ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md)), fed by the builder
|
||||||
|
and trusted by every node's package manager; not built, and not this module's to build. The vendor's own
|
||||||
|
installer is rejected: it puts a self-updating binary under the person's home, invisible to the mesh.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Check | Defends |
|
||||||
|
|---|---|
|
||||||
|
| the module's definition names no node, path or login, declares nothing under a home or `/etc`, and no file resource carries a secret | ADR 0112, ADR 0155, ADR 0183 |
|
||||||
|
| on a lab machine with an account and a seeded home holding a person's rule file and the predecessor's leftovers: after assign, the managed directory holds the mesh's files, the home is byte-identical except the credentials file, which is owned by the operator and names no refresh token; after unassign, the managed directory's files are gone and the home is untouched | ADR 0182, the host's agnosticism |
|
||||||
|
| on a lab machine with no account, the assignment is refused naming the fact | ADR 0181 |
|
||||||
|
| a switch asked of the seat through the console changes the licence and the token on the node; no tool answer and no log line holds a token | ADR 0183 |
|
||||||
|
| the API-key binding writes nothing under the home and the agent authenticates through the helper | ADR 0183 |
|
||||||
|
| the console's provision resolves by co-location; a machine without the console refuses the module by name | ADR 0027, ADR 0152 |
|
||||||
|
| a new session on the assigned workstation lists the console's tools under `mesh` and answers "which node am I" from the instruction file | the exit of the build |
|
||||||
|
|
||||||
|
## What this does not settle
|
||||||
|
|
||||||
|
- **Several operator accounts on one node** (ADR 0181 decides one).
|
||||||
|
- **A worker's own licence on a machine.** Every interactive session shares the node's one agent
|
||||||
|
directory and its licence, however many run. A worker runs from a home of its own with an agent
|
||||||
|
directory in it, bound to its own licence through the manager (to-be 39 §5); that is for when workers
|
||||||
|
exist, and nothing here changes for it.
|
||||||
|
- **The package repository seat** (§7).
|
||||||
|
- **How the module's tools are run** is decided: the node's tool runtime, host-side
|
||||||
|
([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
|
||||||
|
The managed files and the credential write are tools of this module that runtime serves. Until the
|
||||||
|
runtime exists on every node, the module's code runs as a supervised process of its own
|
||||||
|
([ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)),
|
||||||
|
which changes nothing in what it writes.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md),
|
||||||
|
[ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md),
|
||||||
|
[ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) — the decisions
|
||||||
|
- [39 — The Anthropic licence manager](39-the-anthropic-licence-manager.md), [34 — The console](34-the-console.md), [29 — A node has operator accounts](29-a-node-has-operator-accounts.md)
|
||||||
|
- [research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md) — the operator's machine as modules, and where tools run
|
||||||
|
- the vendor's documentation on managed settings, managed tool servers, the managed instruction file and the key-helper, read 2026-10-02
|
||||||
|
- the predecessor's two modules and the six files on the workstations, read 2026-10-02
|
||||||
@@ -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,210 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
- 02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 38. Building the operator's machine
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
*Built and proven 2026-10-02* (mesh-tools, branch `feat/the-operators-machine`, commit `6390d1d`).
|
||||||
|
|
||||||
|
**WP1b — the launcher beside the loader** ([ADR 0188](../../02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)).
|
||||||
|
*mesh-tools, mesh-sdk. A day for the skeleton.* A bundle whose entry is not JavaScript is launched
|
||||||
|
as a child process with the runtime's environment and spoken to over MCP on stdio: `tools/list`
|
||||||
|
once, `tools/call` per call; a tool named `<seat>.<verb>` is the seat's implementation. A child
|
||||||
|
that exits is named as a failed bundle and restarted on the next call. The TypeScript import stays
|
||||||
|
as the shortcut. Beside it, one skeleton SDK per language of the first set — the stdio loop and the
|
||||||
|
tool-definition type, nothing else — each proven by one bundle in that language answering one tool
|
||||||
|
in the runtime's test. **Proof.** The runtime's test: a bundle in a second language, launched, its
|
||||||
|
tool answering on its subject over a real bus; the TypeScript fixture served through the protocol
|
||||||
|
with the shortcut off answers the same.
|
||||||
|
|
||||||
|
## WP2 — The controller composes one runtime per node
|
||||||
|
|
||||||
|
*mesh-controller. Two to three days; the largest package.*
|
||||||
|
|
||||||
|
**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 — each as
|
||||||
|
`<module>=<path>`, and the runtime decides from the file whether it is loaded or launched
|
||||||
|
(WP1b) — `MESH_OPERATOR_ACCOUNT` and
|
||||||
|
`MESH_OPERATOR_HOME` from the account fact, `restart-on` naming every bundle so a push that
|
||||||
|
changes one restarts it. A node with no account composes the runtime without the two words.
|
||||||
|
4. **The gate.** A manifest declaring `tools` and a container built on the runtime's base image is
|
||||||
|
refused at registration once the runtime module is registered, naming this record. It is the
|
||||||
|
mechanism that keeps the old pattern from returning by habit. ADR 0188 widens it, after WP4:
|
||||||
|
a module whose own code is an image artifact is refused, whatever image it is built on.
|
||||||
|
|
||||||
|
**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.
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
---
|
||||||
|
layer: to-be
|
||||||
|
status: designed
|
||||||
|
code: []
|
||||||
|
updated: 2026-10-02
|
||||||
|
decisions:
|
||||||
|
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
|
||||||
|
- 02-DECISIONS/0024-model-access-is-a-provision.md
|
||||||
|
- 02-DECISIONS/0050-model-access-is-vendor-agnostic.md
|
||||||
|
- 02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md
|
||||||
|
- 02-DECISIONS/0113-the-vault-makes-every-secret.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/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 39 — The Anthropic licence manager
|
||||||
|
|
||||||
|
**One module knows every Anthropic licence the mesh has, keeps each alive, decides which consumer gets
|
||||||
|
which, and hands every node's agent its token over the bus.**
|
||||||
|
[ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
|
||||||
|
decides it; this is the shape. It is the successor of the predecessor's manager module, built from what
|
||||||
|
that module learned the hard way, and the counterpart of [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md),
|
||||||
|
which is the consumer on every node.
|
||||||
|
|
||||||
|
## 1. What it is
|
||||||
|
|
||||||
|
A module, `claude-licence-manager`, holding the mesh-scoped seat **`anthropic-licence-manager`**. One
|
||||||
|
holder, on the node the operator assigns it to — the control node is the natural one, and nothing in the
|
||||||
|
definition says so. It requires a database for its own store and the bus; it claims the seat; it serves
|
||||||
|
the seat's verbs. It has no port, no route, no file under anyone's home.
|
||||||
|
|
||||||
|
Its store holds four things:
|
||||||
|
|
||||||
|
| table | holds |
|
||||||
|
|---|---|
|
||||||
|
| **licences** | name, kind (`subscription` or `api-key`), the account's identity (id, address, organisation) once adopted, the grant encrypted at rest, when the access token expires, when the refresh token expires, consecutive failures, the refresh lease, when a person was last notified |
|
||||||
|
| **bindings** | one row per consumer: kind (`node-agent`, `node-session`, `worker`), its key (the node, or the node and the worker), the licence, or *inherit* |
|
||||||
|
| **usage** | the vendor's readings per licence per period, raw beside normalised ([ADR 0054](../../02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md)) |
|
||||||
|
| **audit** | every switch, adoption, refusal and drift, with who asked |
|
||||||
|
|
||||||
|
**The grants are encrypted with a key the vault made for the manager** — its one `secret` requirement.
|
||||||
|
The vault keeps that key; the manager keeps the grants. That is ADR 0050's carve-out, one module, one
|
||||||
|
node, the long-lived grants only.
|
||||||
|
|
||||||
|
## 2. The licences it manages today
|
||||||
|
|
||||||
|
Two subscription accounts and one API key. They differ in kind and the manager treats them so:
|
||||||
|
|
||||||
|
| kind | what the grant is | refresh | what a node is handed | how the agent uses it |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `subscription` | an OAuth grant: an access token that lives hours and a refresh token that lives weeks | the manager rotates it, alone | the access token only | written into the agent's credentials file by the agent module, as the operator |
|
||||||
|
| `api-key` | a key the operator obtained from the vendor | none; a new key is a new adoption | the key | served to the agent through its key-helper setting; nothing is written under the home |
|
||||||
|
|
||||||
|
## 3. Keeping a grant alive
|
||||||
|
|
||||||
|
Carried from the predecessor, where each rule was earned by an incident:
|
||||||
|
|
||||||
|
- **One rotation source.** Only this module calls the vendor's token endpoint. An OAuth refresh is
|
||||||
|
presumed to rotate the refresh token, so a second refresher presenting the old one would kill the
|
||||||
|
grant; whether that presumption holds is to be measured in the lab, and the design is safe either way.
|
||||||
|
- **A lease per licence**, taken in the store before the row is read. A duplicate run sees the token its
|
||||||
|
predecessor just wrote, finds hours of life on it, and does nothing.
|
||||||
|
- **An expiry floor and a cadence.** Within an hour of expiry a refresh must happen; otherwise a grant is
|
||||||
|
rotated once it is older than a declared setting, so a node that misses one rotation still holds hours
|
||||||
|
of life and a broken refresh surfaces in minutes rather than the next morning.
|
||||||
|
- **Failure is counted and escalated once.** Consecutive failures are recorded; past a threshold a
|
||||||
|
notification is emitted, and at most once a day while it stays broken — the predecessor sent one alarm
|
||||||
|
411 times in 35 hours and the incident went unnoticed inside its own alarm.
|
||||||
|
- **A refresh token's own expiry is warned about three days ahead**, because the only remedy is a person
|
||||||
|
logging in again.
|
||||||
|
- **The vendor's reason is logged**, never only the status code: a malformed request and a revoked grant
|
||||||
|
both answer 400, and the predecessor built three concurrency fixes for a bug that was a wrong client id.
|
||||||
|
|
||||||
|
## 4. Handing a token to a node
|
||||||
|
|
||||||
|
Every node that runs the agent module registers that module's public key with the seat when it first
|
||||||
|
runs. From then on:
|
||||||
|
|
||||||
|
- **On rotation**, the manager calls `claude-code.apply@<node>` on every node bound to the rotated
|
||||||
|
licence, with the new token sealed to that node's module key. The module answers *applied*, or
|
||||||
|
*refused* and why, and the manager records it.
|
||||||
|
- **On a switch**, the same call with the other licence's token, and the binding is the authority: the
|
||||||
|
module applies a bind without comparing expiries, because across two licences the numbers are
|
||||||
|
unrelated.
|
||||||
|
- **On a pull** — the module starting, or finding its token near expiry — the module calls the seat's
|
||||||
|
`current` verb for its binding and is answered sealed the same way.
|
||||||
|
- **Never as an event.** What the manager emits names the licence and the outcome and carries no token.
|
||||||
|
|
||||||
|
A node whose module has not registered a key cannot be handed a token, and the manager says so by name
|
||||||
|
rather than falling silent. A node whose module refuses — a wrong identity, a stale grant within one
|
||||||
|
lineage — is recorded as drift and reported.
|
||||||
|
|
||||||
|
## 5. Who gets which licence
|
||||||
|
|
||||||
|
Three consumer kinds, the predecessor's touchpoints with their fallbacks:
|
||||||
|
|
||||||
|
| consumer | bound by | falls back to | if the bound licence cannot be served |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **the node's interactive agent** | the node | nothing: an unbound node has no licence and the agent says so | keeps the last token, which expires within hours; a notification is emitted |
|
||||||
|
| **the mesh's session on a node** | the node, for that session | the node's agent licence | refused |
|
||||||
|
| **a worker** | the worker | the node's session licence, then the node's | refused: a worker never borrows a person's account |
|
||||||
|
|
||||||
|
**One agent directory per machine, shared by every interactive session**, so a node's binding is the
|
||||||
|
licence of all its sessions at once. A worker is a consumer of its own because it runs from a home of its
|
||||||
|
own, with its own agent directory and credentials file, which the agent module on that node writes for
|
||||||
|
it as it writes the operator's — the predecessor ran its agents exactly so.
|
||||||
|
|
||||||
|
**Binding is a person's act through the seat's verbs**, listed by the console: `bind`, `switch`,
|
||||||
|
`release`. **Exhaustion is observed, not acted on**: usage is read every few minutes, a crossing of a
|
||||||
|
declared threshold in the five-hour window is notified once per crossing, and moving a consumer is the
|
||||||
|
operator's call. Switching remains a reaction, not a declaration
|
||||||
|
([ADR 0024](../../02-DECISIONS/0024-model-access-is-a-provision.md)), and an automated policy — move to the
|
||||||
|
least-used licence, stay off a dying one — is designed later if wanted, on the readings this module
|
||||||
|
already keeps.
|
||||||
|
|
||||||
|
## 6. Adopting a grant
|
||||||
|
|
||||||
|
A licence enters the mesh one of two ways, and the token never passes through a prompt, a terminal or an
|
||||||
|
argument:
|
||||||
|
|
||||||
|
- **From a node's login.** A person logs in on a node, as they always have. The agent module there reads
|
||||||
|
the account's identity from the agent's own state file, and offers the full grant to the seat sealed
|
||||||
|
to the manager's key. The manager adopts it into the licence the node is bound to **only if the
|
||||||
|
identity matches** that licence's recorded account; a licence not yet identified is identified by its
|
||||||
|
first adoption; a mismatch is refused and notified, because the predecessor once filed one account's
|
||||||
|
grant into another's row this way.
|
||||||
|
- **An API key** is delivered to the manager by the operator through the seat's `adopt` verb from a file
|
||||||
|
on the manager's node, never as an argument.
|
||||||
|
|
||||||
|
## 7. What it emits and serves
|
||||||
|
|
||||||
|
**Events**, no secret in any: `licence.rotated`, `licence.switched`, `licence.adopted`,
|
||||||
|
`licence.failing`, `licence.refused`, `usage.read` — the audit logger records them all.
|
||||||
|
|
||||||
|
**The seat's verbs**, the contract every future holder must serve: `licences` (each with kind,
|
||||||
|
identity, expiry, failures, who is bound), `bindings`, `bind`, `switch`, `release`, `refresh` (now, one
|
||||||
|
or all), `usage` (current and history), `adopt`, `register` (a node's module key), `current` (a
|
||||||
|
consumer's token, sealed, asked by the consumer's module).
|
||||||
|
|
||||||
|
## 8. Settings
|
||||||
|
|
||||||
|
The refresh cadence; the usage threshold; the notification cooldown. Each declared with a default, so
|
||||||
|
one definition serves and one mesh may differ.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Check | Defends |
|
||||||
|
|---|---|
|
||||||
|
| two refresh runs started together rotate one grant once; the second does nothing and says so | ADR 0183, one rotation source |
|
||||||
|
| every event the manager emits is free of any token; the hand-over opens only with the receiving module's key | ADR 0183, to-be 32 §10 |
|
||||||
|
| a worker bound to a dead licence is refused, never answered with another licence's token | ADR 0183, the fallbacks |
|
||||||
|
| a grant offered with a mismatching identity is refused and one notification emitted | ADR 0183, attribution |
|
||||||
|
| a failing licence notifies once, and once a day after, not once per tick | §3 |
|
||||||
|
| the console lists the seat's verbs and `switch` changes a workstation's token end to end | ADR 0132, the exit of the build |
|
||||||
|
|
||||||
|
## What this does not settle
|
||||||
|
|
||||||
|
- An automated switch on exhaustion (§5).
|
||||||
|
- Whether an OAuth refresh token is single-use; the lab measures it, and §3 holds either way.
|
||||||
|
- How the mesh's own session and a worker read their token on a node once those exist
|
||||||
|
([to-be 15](15-the-agent-session.md), [ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md)):
|
||||||
|
the agent module on that node is their local source, and the reading is theirs to design.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) — the decision
|
||||||
|
- [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md) — the consumer on every node
|
||||||
|
- [14 — Model access](14-model-access.md) — the vendor-blind provision this sits beside
|
||||||
|
- the predecessor's `claude-licences` module: the lease, the floor, the cadence, the cooldown, the identity guard — read 2026-10-02
|
||||||
@@ -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: [hal, hq]
|
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
|
||||||
@@ -169,3 +169,40 @@ them into. The record stays open, and its answer is no longer "index this reposi
|
|||||||
is whatever the mesh grows as its own knowledge surface, if it grows one. Until then the honest fix
|
is whatever the mesh grows as its own knowledge surface, if it grows one. Until then the honest fix
|
||||||
is the README, which should stop claiming a property nothing provides.
|
is the README, which should stop claiming a property nothing provides.
|
||||||
|
|
||||||
|
|
||||||
|
## Where this stands, 2026-09-30
|
||||||
|
|
||||||
|
The README no longer claims a property nothing provides: it says the indexing never existed, that the
|
||||||
|
store it named is unreachable since the cut-over, and that ADR 0025's answer — read, not copied, by an
|
||||||
|
agent that consults this repository — is decided and not built. That was the honest fix the previous
|
||||||
|
note asked for, and it is done.
|
||||||
|
|
||||||
|
The record stays open on ADR 0025's build, and on nothing else. The mesh's own operator surface is now
|
||||||
|
a module ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)); the
|
||||||
|
reader ADR 0025 describes is the mesh session of design 15, which would answer through that surface
|
||||||
|
like any tool. What closes this is still the check 0025 names: search for a phrase that appears only in
|
||||||
|
a design document here, and get it back.
|
||||||
|
|
||||||
|
## Built, 2026-09-30
|
||||||
|
|
||||||
|
[ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md): the
|
||||||
|
reader is a module, `records` (mesh-catalog PR 183), keeping a checkout of this repository from the
|
||||||
|
forge and answering `records_search`, `records_read`, `records_list`, `records_status` and
|
||||||
|
`records_sync` at the commit it read; the console lists them beside every other tool, which is where
|
||||||
|
"beside everything else" lives in a mesh with no store. Design
|
||||||
|
[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
|
||||||
|
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.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user