Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e306fcb4c | ||
|
|
4af731df19 | ||
|
|
7b1dabbce0 | ||
|
|
2caa5e827b | ||
|
|
179fd7f83f | ||
|
|
e1203e5a43 | ||
|
|
4ce967619a | ||
|
|
6df2cfecd6 | ||
|
|
73047501f6 | ||
|
|
bf39baf104 | ||
|
|
5eadf36937 | ||
|
|
8c9a2c7501 | ||
|
|
54213ba90c | ||
|
|
7fb59bde98 | ||
|
|
a4d24d7b65 | ||
|
|
116b2d1793 | ||
|
|
68a14493c9 | ||
|
|
98eb3aa76f | ||
|
|
91bbe648a8 | ||
|
|
0e7b85f184 | ||
|
|
dac49de6e7 | ||
|
|
0d9208dbbf | ||
|
|
bf0ee7cb25 | ||
|
|
4567e13071 | ||
|
|
aa5d9f1045 | ||
|
|
79642251a1 | ||
|
|
331cb94c6e |
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -95,3 +100,21 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
|
|||||||
A new name for an existing thing lands here first, in the same change that introduces it in code. A
|
A new name for an existing thing lands here first, in the same change that introduces it in code. A
|
||||||
record under `02-DECISIONS/` keeps whatever word it was written with — those are immutable — so a
|
record under `02-DECISIONS/` keeps whatever word it was written with — those are immutable — so a
|
||||||
term retired here may still appear there, and the mapping above is how to read it.
|
term retired here may still appear there, and the mapping above is how to read it.
|
||||||
|
|
||||||
|
## The operator's machine
|
||||||
|
|
||||||
|
- **node tools** — the one tool runtime per node, a host-side process the host supervises, that loads
|
||||||
|
every assigned module's tools bundle and serves every tool and held seat's verb on the subjects the
|
||||||
|
memberships issue; its serving mode on loopback is what was called **the console**
|
||||||
|
([ADR 0175](../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
|
||||||
|
Replaces **"console"** as the module's name; *console* remains the word for the person's end of it.
|
||||||
|
- **bundle** — the artifact a module's tools are built into, interpreted or compiled; never an image.
|
||||||
|
- **kept region** — a marked block in a managed file the mesh writes *into*, where the operator's own
|
||||||
|
lines survive every push and are given back when the module goes
|
||||||
|
([ADR 0174](../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)).
|
||||||
|
One of the two ways a node varies a module; the other is a **setting**.
|
||||||
|
- **installed / holding** — a module may be assigned (its package installed, its files placed) without
|
||||||
|
holding the seat its family declares; *holding* is being the one — the login shell, the display
|
||||||
|
session — on that node ([ADR 0176](../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)).
|
||||||
|
- ~~flavor~~ — not used. What a flavor varied is a setting or a separate module.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
status: graduated
|
||||||
|
initiated: 2026-10-02
|
||||||
|
touches:
|
||||||
|
- 02-DECISIONS/0040-what-a-module-is.md
|
||||||
|
- 02-DECISIONS/0011-managed-files-are-generated-never-edited.md
|
||||||
|
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||||
|
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
||||||
|
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
||||||
|
- 02-DECISIONS/0161-what-deserves-a-seat.md
|
||||||
|
- 03-DESIGN/01-to-be/05-the-node-host.md
|
||||||
|
- 03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md
|
||||||
|
- 03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md
|
||||||
|
- 03-DESIGN/01-to-be/34-the-console.md
|
||||||
|
- 03-DESIGN/00-as-is/10-module-catalogue.md
|
||||||
|
- 04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md
|
||||||
|
- 04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md
|
||||||
|
became:
|
||||||
|
- 03-DESIGN/01-to-be/37-the-operators-machine.md
|
||||||
|
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
|
||||||
|
- 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md
|
||||||
|
- 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
|
||||||
|
- 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md
|
||||||
|
- 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 018 — The operator's machine as modules
|
||||||
|
|
||||||
|
**What.** The mesh owns the whole machine, not only the services on it. Everything a person
|
||||||
|
configures on a node — the login manager, the display server, the window manager, the shell, the
|
||||||
|
terminal, the launcher, the notifier, the audio setup, the boot images, the downloads folder, the
|
||||||
|
agent at the terminal — is a module: a package, the files it owns under `/etc` and under the
|
||||||
|
operator's home, the seat it holds, the tools it serves. One default configuration per module,
|
||||||
|
varied per node only through settings rendered into the file or a kept operator region, never
|
||||||
|
through an edit. The servers take the universal modules (shell, prompt, git, the agent); the
|
||||||
|
workstations take those and the graphical stack, which a capability the machine reports gates.
|
||||||
|
This effort writes that behaviour down, measures what the predecessor's desktop modules actually
|
||||||
|
contain, and settles what the mesh must gain before the first of them can be written.
|
||||||
|
|
||||||
|
**Why.** The predecessor is retired on every node. What it still owned on the two workstations —
|
||||||
|
about thirty modules' worth of dotfiles, user units and `/etc` files — is now owned by nothing:
|
||||||
|
no generator regenerates them, and a fix to one of them is a hand edit that nothing records. The
|
||||||
|
migration scoped these modules out as *the workstation's own environment*, and
|
||||||
|
[to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) names them as the last
|
||||||
|
thing the predecessor was keeping alive. To-be 29 covers one directory, `~/.ssh`, and draws a
|
||||||
|
boundary inside it. The operator wants no boundary: the machine is the mesh's, as far as it makes
|
||||||
|
sense to configure it. That is a wider scope than any design states, and it reaches three records
|
||||||
|
that were written for services: what a module is, where a module's tools run, and what a managed
|
||||||
|
file may be.
|
||||||
|
|
||||||
|
**What it touches.** The module definition ([ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md)),
|
||||||
|
seats and their contracts ([ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md)),
|
||||||
|
where a module's tools run ([ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md),
|
||||||
|
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
|
||||||
|
[to-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §6), the host's vocabulary
|
||||||
|
([to-be 05](../../03-DESIGN/01-to-be/05-the-node-host.md)), managed files and settings
|
||||||
|
([ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md),
|
||||||
|
[issue 168](../../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)),
|
||||||
|
and the catalogue's shape ([as-is 10](../../03-DESIGN/00-as-is/10-module-catalogue.md)).
|
||||||
|
|
||||||
|
**Documents.**
|
||||||
|
|
||||||
|
- [01 — The intended behaviour](01-the-intended-behaviour.md): the operator's wish, written as
|
||||||
|
how the mesh behaves, in the mesh's own words.
|
||||||
|
- [02 — What exists, and what is missing](02-what-exists-and-what-is-missing.md): the
|
||||||
|
predecessor's desktop modules measured; which records already say what is wanted; the gaps.
|
||||||
|
- [03 — One tool executor per node](03-one-tool-executor-per-node.md): where a module's tools
|
||||||
|
run. The direction the operator set, the evidence for it, and what it supersedes.
|
||||||
|
- [04 — The seats of the environment](04-the-seats-of-the-environment.md): the roles a machine
|
||||||
|
has once, their candidate contracts, and what gates each.
|
||||||
|
|
||||||
|
**What it had to settle, and where each landed.** *(Graduated 2026-10-02.)*
|
||||||
|
|
||||||
|
1. A module is one *managed thing*, software or not, and every module may serve tools — or ADR
|
||||||
|
0040 already says this and only its examples are narrow.
|
||||||
|
2. One tool executor per node, host-side, module-agnostic; which records it supersedes and
|
||||||
|
in what form the console continues.
|
||||||
|
3. Per-node variation is a setting rendered into the file or a kept region, never an edit —
|
||||||
|
ADR 0011 stands — and issue 168 is fixed before any environment module carries a setting.
|
||||||
|
4. User-scoped units on the host's `service` shape, and a service-manager seat whose holder
|
||||||
|
serves the tools about them.
|
||||||
|
5. The operator account stated on every node; today no node record carries one.
|
||||||
|
6. The seats of the environment and their verbs, one record per seat, slowly, because a
|
||||||
|
seat's tools bind every future holder.
|
||||||
|
7. Where the environment modules live: this catalogue, or one of their own as the media chain
|
||||||
|
has; and whether a third-party organisation's tooling belongs in a public catalogue at all.
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# 01 — The intended behaviour
|
||||||
|
|
||||||
|
*Written 2026-10-02 from the operator's words, in the mesh's words. What is wanted, before what
|
||||||
|
exists. Where a sentence restates a record, the record is named; where it goes further, that is
|
||||||
|
said.*
|
||||||
|
|
||||||
|
## The machine is the mesh's
|
||||||
|
|
||||||
|
**Everything configurable on a node is declared by a module.** Not only the services the mesh
|
||||||
|
runs: the login manager, the display server, the window manager, the bar, the launcher, the
|
||||||
|
notifier, the compositor, the lock screen, the terminal emulator, the clipboard, the shell and its
|
||||||
|
prompt, the editor, the audio setup, the boot images, the package manager's configuration, the
|
||||||
|
agent a person runs at a terminal, and the folders a person works in — a downloads folder that is
|
||||||
|
tidied, backed up, distributed to other nodes and asked questions of. System folders and the
|
||||||
|
operator's home alike. The operator is the only person on every node, so the mesh manages the
|
||||||
|
person's machine, not a machine with a person on it.
|
||||||
|
|
||||||
|
This is [ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md)'s definition applied without
|
||||||
|
the service bias its examples carry. A module is one managed thing, named once, described
|
||||||
|
completely by its manifest. It may have a package, files, a container, a unit, a binary, a seat it
|
||||||
|
holds, and tools it serves — any one of these, or all, or two. There is **no kind of module**: zsh
|
||||||
|
has a package, files, a seat claim and the tools that claim obliges it to serve; downloads has a
|
||||||
|
folder, a process and tools; nftables has a package, files, a service, a seat and tools. The
|
||||||
|
difference is what each declares, not what each is.
|
||||||
|
|
||||||
|
**The home has no boundary.** [To-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md)
|
||||||
|
owns one directory under the home and draws a line inside it between the mesh's and the person's.
|
||||||
|
Here the line is drawn only by what the modules declare: every file some module places is the
|
||||||
|
mesh's; what no module declares is found and left alone, exactly as the adoption rules already
|
||||||
|
say for a machine. The reach is bounded by sense, not by a rule — the mesh configures what can
|
||||||
|
be configured, and a person's documents, projects and history are data under
|
||||||
|
[ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md), not configuration.
|
||||||
|
|
||||||
|
**A module names no node and no path.** The operator account is a node fact and the home is
|
||||||
|
derived from it ([to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §1–2,
|
||||||
|
shipped in the controller; its record is proposed in an open change). A module places a file
|
||||||
|
*under the home, owned by the account*, and the same manifest lands on a server and a laptop.
|
||||||
|
|
||||||
|
## One default, varied by settings, never by edits
|
||||||
|
|
||||||
|
**One module, one default configuration.** The window manager module ships the configuration
|
||||||
|
that is right for every node. There are no flavors: the predecessor's one desktop module carried
|
||||||
|
four, one per class of machine, and what differed between them is what settings are for.
|
||||||
|
|
||||||
|
**A node varies a module in exactly two ways.** A **setting**, declared by the module with its
|
||||||
|
type, meaning and default (proposed alongside the container-runtime records), set for the mesh
|
||||||
|
or for one node, and rendered into the file at composition — the value is in the file, not in an
|
||||||
|
environment variable the file reads. Or a **kept region**: a block in a file the mesh writes
|
||||||
|
*into*, where the operator's own lines survive every push
|
||||||
|
([ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md)). An
|
||||||
|
edit to a managed file outside such a region is not a third way; it is overwritten, as
|
||||||
|
[ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md) says, and the
|
||||||
|
predecessor's habit of adopting disk drift back into its database is not carried over.
|
||||||
|
|
||||||
|
The predecessor's theming — some ninety environment variables substituted into templates at sync
|
||||||
|
time, with tools to list and set them — is the same idea with the wrong rendering. The knobs
|
||||||
|
become declared settings; the file carries the value.
|
||||||
|
|
||||||
|
## Roles a machine has once are seats, and seats carry tools
|
||||||
|
|
||||||
|
**A role a machine fills at most once is a node-scoped seat**, declared by a module
|
||||||
|
([ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md),
|
||||||
|
[ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)): the login shell, the
|
||||||
|
display session, the display server, the terminal emulator, the launcher, the notifier, the
|
||||||
|
compositor, the lock screen, the service manager, the boot loader. Several modules may be able to
|
||||||
|
hold one — zsh, fish and bash can all hold the login shell — and the assignment on each node says
|
||||||
|
which does. Installing a shell is installing software; holding the seat is being *the* shell.
|
||||||
|
|
||||||
|
**A seat's contract is its tools** ([ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
|
||||||
|
Every holder of the login-shell seat serves `execute`, which takes one string, the command, and
|
||||||
|
runs it on the node the seat is scoped to. Every holder of the boot seat serves "rebuild the boot
|
||||||
|
images", so *"rebuild your boot images"* is a verb addressed to a machine, not a one-off step in
|
||||||
|
a hook. Every holder of the service-manager seat answers for the units on the machine, system and
|
||||||
|
user scope. A module may serve its own tools beside the seat's
|
||||||
|
([ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) §2): show the rendered
|
||||||
|
configuration, set a theme value, report status.
|
||||||
|
|
||||||
|
**Any tool may be called from any node.** The operator's statement, and the grant model it
|
||||||
|
implies: the executor on each node may call everything, as the console already may. A verb that
|
||||||
|
needs root on the machine is the module's concern — the tool escalates, the executor and the
|
||||||
|
caller do not know.
|
||||||
|
|
||||||
|
## Servers and workstations differ by capability, not by catalogue
|
||||||
|
|
||||||
|
The same catalogue serves every node. A module declares what it needs — a graphical session, a
|
||||||
|
display server, a container runtime — and the machine reports what it has, as the profile already
|
||||||
|
reports eight capabilities today ([issue 160](../../04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md)).
|
||||||
|
Assignment refuses the wrong placement by name
|
||||||
|
([ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md) §3). So every node takes the shell,
|
||||||
|
the prompt, git and the agent; only a node with a graphical session can take the display server,
|
||||||
|
and only a node holding the display server can take a window manager. Nothing in a module says
|
||||||
|
"workstation".
|
||||||
|
|
||||||
|
## What the operator would say to the mesh
|
||||||
|
|
||||||
|
*Set the login shell on the build node to fish. Rebuild the laptop's boot images. Show me the
|
||||||
|
window manager's effective configuration on the desktop and where each value comes from. Give
|
||||||
|
the downloads folder on the laptop to the home server. Run `uptime` on every node.* Each of these
|
||||||
|
is a seat verb or a module tool, addressed to a node, answered by whatever holds the role there.
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
# 02 — What exists, and what is missing
|
||||||
|
|
||||||
|
*Measured 2026-10-02 on one installation: two workstations, two servers, all four converged to
|
||||||
|
the mesh; the predecessor retired on the last workstation the day before. Numbers are from the
|
||||||
|
machines and the repositories, not from memory.*
|
||||||
|
|
||||||
|
## 1. What the predecessor's desktop looks like
|
||||||
|
|
||||||
|
The predecessor's catalogue on the laptop held **34 modules**, of which **28** are the operator's
|
||||||
|
environment rather than services. By what they declare:
|
||||||
|
|
||||||
|
| shape | count | examples |
|
||||||
|
|---|---|---|
|
||||||
|
| package only | 9 | browser, mail client, process monitor, media player, file manager, chat |
|
||||||
|
| package + `/etc` files + system service | 5 | login manager, display server, power and thermal daemons, package manager configuration |
|
||||||
|
| package + files under the home | 6 | shell and prompt, the agent at the terminal, scripts, the sync client, a music player |
|
||||||
|
| files under the home + user units + hooks | 2 | the desktop environment, audio |
|
||||||
|
| third-party organisation tooling | 6 | out of scope here |
|
||||||
|
|
||||||
|
**The desktop module alone** declares **88 files**, **4 flavors** (the window-manager stack, and
|
||||||
|
one per class of machine), **2 user units** with a hook to enable them, 8 files under `/etc`, a
|
||||||
|
wallpaper shipped as an asset, and reads **about 90 environment variables** as theme knobs,
|
||||||
|
substituted into its templates at sync time and set through a theming tool. Its hook exists
|
||||||
|
because *shipping a unit file does not run it*: one unit had been deployed for months and ran on
|
||||||
|
one machine only, because somebody had enabled it there by hand.
|
||||||
|
|
||||||
|
**The shell module** ships `~/.zshrc`, the prompt configuration, an `~/.ssh/config` that the
|
||||||
|
predecessor generated from its registry, and a `LOGIN_SHELL` variable applied with `chsh` by a
|
||||||
|
hook. Two flavors: the prompt theme, and autocompletion.
|
||||||
|
|
||||||
|
**Other modules write into the desktop module's files.** The chat client places i3 and notifier
|
||||||
|
snippets into `config.d` directories the desktop module owns, and its launch flags, window
|
||||||
|
placement and notification colours are each a variable with a default.
|
||||||
|
|
||||||
|
**One-off steps live in hooks** across the set: enable user units, `chsh`, create a swap file,
|
||||||
|
`mkinitcpio`, enable a vendor VPN service the package ships disabled. Every one is state the
|
||||||
|
host could declare or a verb a seat could serve; none is today.
|
||||||
|
|
||||||
|
## 2. What the migration did with them
|
||||||
|
|
||||||
|
The migration's module to-do scoped the whole set out as *desktop / workstation ricing — the
|
||||||
|
workstation's own environment* and *node/OS tooling — managed on the node, never catalogue*. The
|
||||||
|
last workstation's runbook then split the same set three ways: **A**, system scope, which the
|
||||||
|
host's vocabulary can express today (the login manager, the display server, the power daemons,
|
||||||
|
the package manager, the container runtime); **B**, under a home or a user unit, waiting on
|
||||||
|
to-be 29; **C**, package only, the operator's call. The migration log closes the workstation with
|
||||||
|
*the operator's desktop awaiting its design*.
|
||||||
|
|
||||||
|
Two things followed from scoping them out. Nothing regenerates those files now, so a fix is a hand
|
||||||
|
edit — the login manager's session script was fixed this way on the day of writing, and recorded
|
||||||
|
in a repository nothing deploys from. And the one piece of this family written as a mesh module,
|
||||||
|
the ssh client, was closed on hold in the catalogue until the controller carried the account fact.
|
||||||
|
|
||||||
|
## 3. What the records already give
|
||||||
|
|
||||||
|
| wanted | record | state |
|
||||||
|
|---|---|---|
|
||||||
|
| one module per managed thing; every module may have tools | [ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md) | accepted; examples are services, and the shell is named as a *shared* seat |
|
||||||
|
| a module declares its own node-scoped seat | [ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) | accepted |
|
||||||
|
| a seat's contract is its tools; a holder may add its own | [ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) | accepted; one node seat serves verbs live |
|
||||||
|
| a capability the machine reports gates a holder | [ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md) §3 | accepted; the profile already reports `graphical-session` |
|
||||||
|
| the account as a node fact; a file under the home owned by it | [to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §1–2 | built in the controller; its record proposed in an open change |
|
||||||
|
| inside a home: owned, written into, written by the module, found | proposed in the same change | proposed |
|
||||||
|
| a setting declared with type, meaning, default and cost | proposed with the container-runtime records | proposed |
|
||||||
|
| a managed file is derived; an edit is overwritten | [ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md) | accepted |
|
||||||
|
| the mesh writes into a shared file, never over it | [ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md) | accepted |
|
||||||
|
| a module names no path; the host resolves the home | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) | accepted |
|
||||||
|
| the `user` shape: a login shell is declared state | [to-be 05](../../03-DESIGN/01-to-be/05-the-node-host.md) | designed; used by no module |
|
||||||
|
|
||||||
|
## 4. What is missing
|
||||||
|
|
||||||
|
1. **The account is recorded nowhere.** The node record has the column; on all four nodes it
|
||||||
|
is empty. Every home-scoped module is unassignable until the operator states it.
|
||||||
|
2. **User-scoped units.** The host's `service` shape has no user scope. To-be 29 says it
|
||||||
|
plainly: *a workstation's per-user daemons have no form the mesh can send.* The desktop
|
||||||
|
module's two units, the audio masks, the power module's memory guard and the thermal
|
||||||
|
daemon's profile switcher all need it.
|
||||||
|
3. **One-off steps.** `mkinitcpio`, `chsh`, creating a swap file. Each is either declared
|
||||||
|
state the host lacks a shape for, or a verb a seat should serve. An action in a declaration
|
||||||
|
is refused over the link, and rightly.
|
||||||
|
4. **Settings leak** ([issue 168](../../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)):
|
||||||
|
a setting reaches every mergeable file and every contribution of its module. Ninety theme
|
||||||
|
knobs on that mechanism would reach ninety files. The proposed settings record says a setting
|
||||||
|
names the file it lands in; that has to ship first.
|
||||||
|
5. **Where tools run.** Every module that serves a tool today does so from its own container
|
||||||
|
per node. See [03](03-one-tool-executor-per-node.md).
|
||||||
|
6. **A seat's verbs are undecided for every seat but three.** To-be 33 leaves which verbs each
|
||||||
|
seat serves as *a decision per seat, slowly*. The environment adds a dozen seats.
|
||||||
|
7. **Catalogue placement.** The media chain left this catalogue for its own; whether the
|
||||||
|
environment does the same, and whether a third-party organisation's tooling belongs in a
|
||||||
|
public catalogue, are unasked.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# 03 — One tool executor per node
|
||||||
|
|
||||||
|
*The direction the operator set on 2026-10-02, the evidence it rests on, and what it supersedes.
|
||||||
|
A direction, not yet a decision: the record is written when this effort graduates.*
|
||||||
|
|
||||||
|
## Where tools are served today
|
||||||
|
|
||||||
|
[To-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) names three families — a
|
||||||
|
role's tools on the seat, a module's own tools on the module, the mesh's own verbs on the
|
||||||
|
controller seat — and [ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||||
|
says a runtime serves the subjects its membership issues. What *runs* that runtime is
|
||||||
|
[ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md):
|
||||||
|
one supervised process per module, under the module's own account, carrying that module's
|
||||||
|
compiled tools. Measured on the live mesh:
|
||||||
|
|
||||||
|
| who answers | how it runs | count |
|
||||||
|
|---|---|---|
|
||||||
|
| the mesh's own verbs | the controller binary, on its node | 17 verbs |
|
||||||
|
| the store seat | the store's own runtime | 2 verbs |
|
||||||
|
| the packet-filter seat | **a container per node**, built on the tool-runtime base image, with the network namespace and `NET_ADMIN`, on all four nodes | 3 verbs and 1 own tool |
|
||||||
|
| every module's own tools | the module's container, one per node it runs on | 67 tools across the catalogue |
|
||||||
|
| the console | a container per node, loopback MCP, `invokes: *` | serves none, calls all |
|
||||||
|
| the host | — | serves nothing; answers no question about the machine |
|
||||||
|
|
||||||
|
**The packet-filter holder is the case to look at.** The module is a package, three files and a
|
||||||
|
system service. To serve three verbs it also declares a built image and a container on every
|
||||||
|
node whose only job is to answer them. Scaled to the environment — a shell, a prompt, a launcher,
|
||||||
|
a notifier, a compositor, a login manager, a service manager, a boot loader, a downloads folder —
|
||||||
|
that is one container per module per node for software that is itself not a container, and the
|
||||||
|
operator's judgement is that tools should not run inside a container at all.
|
||||||
|
|
||||||
|
## The direction
|
||||||
|
|
||||||
|
**One tool executor per node, on the host side.** A process the host supervises, the way the
|
||||||
|
launcher supervises the host ([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)): not a
|
||||||
|
container, one bus credential for the node, module-agnostic. It loads the tool code of every
|
||||||
|
module assigned to the node and serves each module's tools and each held seat's verbs on the
|
||||||
|
subjects the membership issues — nothing changes in what [ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
|
||||||
|
and [ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||||
|
say about subjects, grants and memberships; what changes is that one process subscribes for the
|
||||||
|
node instead of one per module.
|
||||||
|
|
||||||
|
- **A module brings its tools as a built artifact**, a bundle the pipeline produces, never an
|
||||||
|
image. The executor knows bundles and subjects; it knows nothing of zsh or nftables.
|
||||||
|
- **A tool is code the module wrote**, one function behind an MCP verb. `execute` on the shell
|
||||||
|
seat is a function with a string argument. The executor does not declare, template or
|
||||||
|
interpret tools; it runs them.
|
||||||
|
- **Root is the module's concern.** A tool that must change the packet filter or rebuild boot
|
||||||
|
images escalates itself. The executor does not run as root for everyone, and the caller does
|
||||||
|
not know.
|
||||||
|
- **Any node may call any tool on any node.** The executor's credential may call everything,
|
||||||
|
as the console's already does; per-module grants on the calling side are not kept.
|
||||||
|
- **The mesh's own verbs stay with the controller** ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)),
|
||||||
|
and a mesh-scoped seat's verbs run on the node that holds it
|
||||||
|
([ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)).
|
||||||
|
No hub is added; the controller's node is already one.
|
||||||
|
|
||||||
|
**The console is the executor, renamed.** It already runs on every node with a credential that
|
||||||
|
may call everything, and it already serves the mesh's tools to whoever is on the machine over
|
||||||
|
MCP on loopback ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
|
||||||
|
It moves out of its container into the host's process tree, gains the serving half, and takes a
|
||||||
|
name that says what it is — *the node's tool runtime* or simply *node tools*; "console" names
|
||||||
|
the operator's half only.
|
||||||
|
|
||||||
|
## What it supersedes, and what it keeps
|
||||||
|
|
||||||
|
| record | effect |
|
||||||
|
|---|---|
|
||||||
|
| [ADR 0047](../../02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md), [ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md) | superseded *for tools*: one process per node runs every module's tool code, under one account. A module's long-running service — a daemon, a container — is untouched; the executor runs tools, not services. The record must say why one account for every module's tools is acceptable: every tool may be called from every node anyway, and root is taken by the tool, not granted to the process |
|
||||||
|
| [ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md) | kept in substance — a module, assigned per node, loopback MCP, the machine's login is the authority — changed in form: host-side, not a container; serves as well as calls; renamed |
|
||||||
|
| [to-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §6, [to-be 34](../../03-DESIGN/01-to-be/34-the-console.md) | amended the same way |
|
||||||
|
| [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) §3, *a container may ask for a capability* | moot for that holder: the verbs run on the host side and escalate as they need |
|
||||||
|
| the container-runtime seat, proposed in an open change: *the holder runs as a supervised process and serves the verbs locally to the host and on the bus* | consistent — a supervised process serving verbs is what the executor is; the open question is whether that holder keeps its own process or serves through the executor like everyone else |
|
||||||
|
| the tool-runtime base image | no longer the way tools reach a node; may remain the way a module's *service* is built |
|
||||||
|
|
||||||
|
## What stays open
|
||||||
|
|
||||||
|
- **The executor's language.** The host is a static Go binary and loads no plugins, so the
|
||||||
|
executor is a sibling process, and its language decides the language of every tool bundle.
|
||||||
|
One decision, taken once.
|
||||||
|
- **How a bundle reaches the node.** An artifact of the module's build, delivered as the host
|
||||||
|
delivers everything else; whether it is a file resource in the declaration or a thing the
|
||||||
|
executor fetches by digest.
|
||||||
|
- **Reload.** A push that adds or upgrades a module's bundle reaches a running executor as a
|
||||||
|
reload, not a restart, or every tool on the node blinks on every push.
|
||||||
|
- **The host's own questions.** [Issue 160](../../04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md)
|
||||||
|
wants a machine to say more about itself. With an executor on every node, "what is this
|
||||||
|
machine made of" is a seat verb like any other, served there.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# 04 — The seats of the environment
|
||||||
|
|
||||||
|
*Candidates, not decisions. To-be 33 says which verbs a seat serves is a decision per seat,
|
||||||
|
taken slowly, because a seat's tools bind every future holder. This document lists the roles the
|
||||||
|
operator's machine has once, who could hold each, what gates it, and a first verb or two — so
|
||||||
|
each record has a starting point.*
|
||||||
|
|
||||||
|
## The rule for what is a seat here
|
||||||
|
|
||||||
|
A role the machine fills **at most once** is a node-scoped seat, declared by the module family
|
||||||
|
that fills it ([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)). A thing
|
||||||
|
several of which coexist without contention — editors, browsers, media players — is not a seat;
|
||||||
|
each is a module with its own tools, and nothing is singular about it. A seat is held by one
|
||||||
|
assignment per node; other modules of the same family may be installed beside it without
|
||||||
|
holding it ([ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md) §1, read with the sharper
|
||||||
|
distinction: *installed* is not *holding*).
|
||||||
|
|
||||||
|
## Candidate seats
|
||||||
|
|
||||||
|
| seat | holders | gated by | first verbs |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **login shell** | zsh, fish, bash | nothing: universal | `execute(command)`; `show-config`; the holding itself sets the account's login shell through the host's `user` shape |
|
||||||
|
| **service manager** | systemd | the `service-manager` capability the profile reports | units: list, status, start, stop, restart, enable, journal; **user scope** on each |
|
||||||
|
| **boot** | grub, systemd-boot | a machine that boots itself (not a container host) | `rebuild-images`; `entries` |
|
||||||
|
| **package manager** | pacman, apt | the `package-manager` capability | search, installed, upgrade, orphans; today a capability the host uses, not a seat anyone holds |
|
||||||
|
| **display server** | xorg, wayland compositors that are their own server | the `graphical-session` capability | `displays`; `layout` |
|
||||||
|
| **display session** | i3, sway | the display server seat held on the node; i3 needs x11, sway needs wayland | `reload`; `workspaces`; `windows`; `move` |
|
||||||
|
| **terminal emulator** | xterm, alacritty, foot | display session | `open`; `font` |
|
||||||
|
| **launcher** | rofi, dmenu | display session | `show`; `theme` |
|
||||||
|
| **notifier** | dunst, mako | display session | `send`; `history`; `rule` |
|
||||||
|
| **compositor** | picom | display server (x11 only) | `restart`; `effects` |
|
||||||
|
| **lock screen** | i3lock, swaylock | display session | `lock` |
|
||||||
|
| **bar** | i3status-rust, waybar | display session | `reload`; `blocks` |
|
||||||
|
| **login manager** | lemurs, greetd | graphical session | `sessions`; `default-session` |
|
||||||
|
| **audio** | pipewire, pulseaudio | the machine reports a sound device | `sinks`, `sources`, `default`, `volume`, `mute` |
|
||||||
|
| **clipboard** | greenclip, cliphist | display session | `history`; `clear` |
|
||||||
|
|
||||||
|
Not seats, modules with their own tools: the editor, the browser, the mail client, the file
|
||||||
|
manager, the media player, the chat client, the agent at the terminal, the downloads folder, the
|
||||||
|
scripts folder, the sync client, the power and thermal daemons that are specific to one machine's
|
||||||
|
hardware.
|
||||||
|
|
||||||
|
## What the table implies
|
||||||
|
|
||||||
|
**Capabilities come first.** `graphical-session`, `service-manager` and `package-manager` are
|
||||||
|
reported today. *A display server is held* is not a capability but a seat being held, and a
|
||||||
|
module that needs it declares a dependency on the seat, not on a capability: *i3 needs the
|
||||||
|
display server seat held by xorg*. Whether a held seat can gate another's assignment is a
|
||||||
|
question for the controller's resolver, and the first environment module after the shell will
|
||||||
|
ask it.
|
||||||
|
|
||||||
|
**The service manager comes early.** Four of the predecessor's modules ship user units, and the
|
||||||
|
executor itself is a unit. User scope on the host's `service` shape is a host change whichever
|
||||||
|
module holds the seat; the seat's holder answers the questions about units, it does not apply
|
||||||
|
them — the host does, as it does for every declared resource.
|
||||||
|
|
||||||
|
**The shell comes first.** Universal, no capability, one verb that is immediately useful on
|
||||||
|
every node, and the `user` shape already makes the login shell declared state. It is the module
|
||||||
|
that proves the pattern: a package, files under the home owned by the account, a seat claim,
|
||||||
|
tools served by the executor, settings for the few things that vary per node, and a kept region
|
||||||
|
for the operator's own lines.
|
||||||
|
|
||||||
|
**The login manager is the first system-scope one**, because it needs nothing new: a package,
|
||||||
|
two files under `/etc`, a service — the same shape the ssh daemon module has today — and the
|
||||||
|
session script it owns is the file that was hand-fixed the day this effort opened.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
status: active
|
||||||
|
initiated: 2026-10-02
|
||||||
|
touches: [lab, the lab module, the catalogue, assignments, settings, the controller's store]
|
||||||
|
became: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# 019 — A warm twin of the running mesh
|
||||||
|
|
||||||
|
## What is being investigated
|
||||||
|
|
||||||
|
Whether the lab can keep a **warm twin of the mesh as it actually runs**: the same machines, carrying
|
||||||
|
the same catalogue, the same assignments and the same settings as the live mesh, raised once and kept
|
||||||
|
ready, so that a change can be tested against the mesh as it is rather than against a scenario
|
||||||
|
written to resemble it. A run against the twin would go through the lab module like any other run:
|
||||||
|
a branch per repository, the twin restored from its snapshot, the change applied, the beds run.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The lab's beds raise meshes from declarations written for the bed. They prove the mechanism. They
|
||||||
|
do not prove that a change works on the mesh that runs, with its accumulated assignments, its
|
||||||
|
operator settings, its adopted machines and its modules in their real combinations. The gap showed
|
||||||
|
on 2026-10-02:
|
||||||
|
|
||||||
|
- a change to how a module's settings reach its files was correct in every bed, and would have put a
|
||||||
|
setting into the container runtime's configuration on every machine running that module. Only the
|
||||||
|
composed plan for a real machine showed it;
|
||||||
|
- a firewall change composed cleanly and still left one machine's wired port unfiltered, because
|
||||||
|
of a link that machine had and no bed did;
|
||||||
|
- a recovery step was needed on every machine at once, after a change that every bed had passed.
|
||||||
|
|
||||||
|
The lab already has a warm mode, a snapshot of a raised scenario restored between attempts. What it
|
||||||
|
does not have is a scenario that **is** the running mesh, kept current with it.
|
||||||
|
|
||||||
|
## What it touches
|
||||||
|
|
||||||
|
- **What a twin is made of.** The catalogue and the assignments are records; settings are records;
|
||||||
|
secrets are sealed to machines and cannot be copied. Which of these can be carried to the lab as
|
||||||
|
they are, which must be substituted, and how a twin says what it substituted.
|
||||||
|
- **Data.** A twin with the real catalogue and no real data proves composition and delivery, not a
|
||||||
|
migration. Whether a twin carries data, a sample of it, or none.
|
||||||
|
- **Keeping it current.** A twin raised once goes stale with the first merge. Whether it is
|
||||||
|
re-derived from the live records on each run, refreshed on a schedule, or rebuilt only when asked.
|
||||||
|
- **Machines.** The live mesh has machines of different kinds: a server on the internet, machines
|
||||||
|
behind a home router, a laptop that sleeps. Which of their properties a twin must reproduce for a
|
||||||
|
test to mean anything (reachability, the private network, the found firewall).
|
||||||
|
- **Cost.** The lab machine's memory and disk, and how long a twin takes to raise from cold.
|
||||||
|
- **The lab module's tools.** A run against the twin rather than a named bed: one more tool, or an
|
||||||
|
argument to the run tool.
|
||||||
|
|
||||||
|
## Starting point
|
||||||
|
|
||||||
|
The lab module (ADR 0172) runs beds through the mesh, and the lab's warm mode already snapshots and
|
||||||
|
restores a raised scenario. The beds that raise a machine shaped like one live machine from the
|
||||||
|
catalogue are the nearest existing thing, and the first to compare against.
|
||||||
@@ -78,6 +78,8 @@ capability. The host hardcodes no firewall, supervisor, package manager or runti
|
|||||||
generic apply primitives and platform detection, so it runs where none of those exist — an Android
|
generic apply primitives and platform detection, so it runs where none of those exist — an Android
|
||||||
phone has no ufw, systemd, pacman or Docker.
|
phone has no ufw, systemd, pacman or Docker.
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-02, by [ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md).** The shell example above — *bash, zsh and fish all join `shell`; one may be default* — is read as *installed is not holding*: the three may all be installed, and the `login-shell` seat is node-scoped and held by exactly one. The decision — what a module is, and the three relationships — stands; [ADR 0173](0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md) applies it to the operator's whole machine.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- **Supersedes the earlier "grouped by domain" decision** (folded in consolidation; see the
|
- **Supersedes the earlier "grouped by domain" decision** (folded in consolidation; see the
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ extends: 0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md
|
|||||||
|
|
||||||
# 47. A module runs its code as its own process, with its own account
|
# 47. A module runs its code as its own process, with its own account
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** A module's *tools* are no longer served by the module's own process under its own account: one tool runtime per node, on the host side, serves every assigned module's bundle. A tool is still served on its own subject and only the module that serves it answers; what moved is the process and the account.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
A module is one self-contained thing ([ADR 0040](0040-what-a-module-is.md)), and it gets a broker
|
A module is one self-contained thing ([ADR 0040](0040-what-a-module-is.md)), and it gets a broker
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ extends: 02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-ow
|
|||||||
|
|
||||||
# 150. A module's own code runs as supervised processes under the module's one account
|
# 150. A module's own code runs as supervised processes under the module's one account
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** For a module's *tools*, read that record: one runtime per node, the node's one account, bundles loaded from the memberships. This record still governs a module's long-lived processes — a daemon, a provisioner, a scheduled ingest — and the account invariant for them.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
The repository answers "what runs a module's own code" two ways and reconciles them nowhere
|
The repository answers "what runs a module's own code" two ways and reconciles them nowhere
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ operator owns; narrowing what it may call is a setting on its assignment, which
|
|||||||
[ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) already provides for
|
[ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) already provides for
|
||||||
and nothing here builds.
|
and nothing here builds.
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** The surface stays a module assigned per node, on loopback, with the machine's login as the authority. It is no longer a container: it is the node tools runtime's serving mode, host-side, and that runtime also serves every assigned module's tools. The module is renamed `node-tools`.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- **The way a person drives the mesh is inside the mesh.** It is declared, delivered, replaced and
|
- **The way a person drives the mesh is inside the mesh.** It is declared, delivered, replaced and
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ itself restarts, and the console says so rather than hiding the modules' tools w
|
|||||||
**A grant of `*` reaches a role's tools; `seat:<seat>.<verb>` grants one.** The console's `*` needed no
|
**A grant of `*` reaches a role's tools; `seat:<seat>.<verb>` grants one.** The console's `*` needed no
|
||||||
change to reach the mesh's verbs, which is what a grant meaning *every tool* should mean.
|
change to reach the mesh's verbs, which is what a grant meaning *every tool* should mean.
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).**
|
||||||
|
> The seat gains a generic verb beside the named ones: `command`, which takes one command line as the
|
||||||
|
> controller's binary takes it and answers what it printed. The named verbs stand and keep their
|
||||||
|
> schemas; `command` is the whole binary, added because the operator decided any node may call any
|
||||||
|
> tool and a verb per command was the only thing keeping `node account`, `node show` and the rest
|
||||||
|
> behind a shell on the control node. Additive within the version, as §"additive" above allows.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- **The console answers the mesh's own questions.** Issue 147's first paragraph closes: what a node
|
- **The console answers the mesh's own questions.** Issue 147's first paragraph closes: what a node
|
||||||
|
|||||||
-146
@@ -1,146 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: proposed
|
|
||||||
date: 2026-10-01
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 164. A setting is declared with its default, its meaning and what changing it costs
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The operator asked for one thing for every module, with the container runtime as the first case: **one
|
|
||||||
consistent default configuration for every machine, overridable per assignment, and easy to change
|
|
||||||
later.** The four machines' runtime configurations were each written by hand and differ — one keeps
|
|
||||||
running containers through a daemon restart and one does not, their log rotation differs, and each
|
|
||||||
names its resolver and its trusted registries in its own words.
|
|
||||||
|
|
||||||
Most of this was already decided.
|
|
||||||
[ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) said the definition is
|
|
||||||
identity and **defaults**, the assignment's settings are the configuration, *unset is the default*, and
|
|
||||||
*an unknown setting is refused*. [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) made
|
|
||||||
a setting an operator requirement whose contract is "a type and, optionally, a default", answered by
|
|
||||||
"the assignment's, or the requirement's default, or unresolved". An assignment is a module on a node,
|
|
||||||
so the node layer of a module's settings already *is* the per-assignment override, and the mesh-wide
|
|
||||||
layer is the one consistent default a person changes once.
|
|
||||||
|
|
||||||
What was built is narrower than what was decided, measured in the controller on the day of deciding:
|
|
||||||
|
|
||||||
- **Nothing declares which keys are settable.** A mergeable file's content is its defaults, and every
|
|
||||||
key of it — and every key not in it — is accepted. Nothing tells a person, or the console, what can be
|
|
||||||
set, of what type, or what it means.
|
|
||||||
- **The refusal of an unknown setting is not there for most modules.** The stray-setting report returns
|
|
||||||
nothing at all for a module with any mergeable file, because such a file "takes any key"
|
|
||||||
([issue 173](../04-ISSUES/173-a-modules-settings-reach-every-fact-it-contributes/00-report.md) left
|
|
||||||
files that way on purpose). It reports rather than refuses where it does run.
|
|
||||||
- **A value in a file that is not JSON can have no default.** `${setting:<key>}`
|
|
||||||
([ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md)) is refused when no
|
|
||||||
layer sets it — right for a mail domain, where a default is the very literal 0155 removes, and wrong
|
|
||||||
for a tunable like the resolver's upstreams, which the resolver module therefore carries as literals
|
|
||||||
in its file.
|
|
||||||
- **A setting reaches every mergeable file its module owns.** The layers are one flat map per module,
|
|
||||||
laid over each such file. Adding a setting to the resolver module for its own configuration put the
|
|
||||||
key into the container runtime's file as well — the resolver writes into that file too — and the
|
|
||||||
runtime refuses keys it does not know. The plan showed it before any push; the runtime's file was
|
|
||||||
then made to take no settings at all ([issue 198](../04-ISSUES/198-the-lans-dns-server-ran-outside-the-mesh-and-its-filter-closed-it/00-report.md)). Issue 173 stopped settings leaking into
|
|
||||||
contributions and served facts; between one module's own files the leak remains.
|
|
||||||
- **What a change costs is said per file, not per key.** A service names the files it is reloaded or
|
|
||||||
restarted on. The runtime re-reads its trusted registries on a reload and its `dns` key only when it
|
|
||||||
starts; the resolver module declared a reload, so on two machines the key was written, reloaded,
|
|
||||||
and never read, and every container got a public resolver for weeks while everything read as
|
|
||||||
current ([issue 110](../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/01-resolution.md)).
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
1. **Leave settings implicit; document each module's keys in its README.** Rejected: a key the mesh
|
|
||||||
does not know cannot be refused, typed, listed by the console or costed, and a README is a rule
|
|
||||||
enforced by nothing.
|
|
||||||
2. **A second mechanism for tunables beside settings** — defaults in a new block, settings untouched.
|
|
||||||
Rejected: two ways to state one person's value, and design 27 already retires six mechanisms
|
|
||||||
that grew that way.
|
|
||||||
3. **Settings declared in the definition, as 0112's operator requirement: a key, a type, a meaning,
|
|
||||||
optionally a default, and what a change costs.** Adopted.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**A module declares every setting it takes.** Each declared setting has a name, a type, one sentence
|
|
||||||
of meaning, optionally a default, and what a change to it costs. The spelling is design 27's to settle
|
|
||||||
with the rest of the requirement form; this record decides the content.
|
|
||||||
|
|
||||||
**A setting with a default is a tunable; a setting without one is the operator's.** A tunable resolves
|
|
||||||
to its default when no layer sets it — wherever it is read, a mergeable file or `${setting:<key>}` in a
|
|
||||||
file of any format. A setting with no default is refused by name when nothing sets it, as 0155 decided;
|
|
||||||
0155's refusal is narrowed to exactly that case, not changed for it. Whether a value has a default is a
|
|
||||||
fact about the software (a log size does, a mail domain does not), and the definition states it once.
|
|
||||||
|
|
||||||
**The layers stay as they are, and every value says where it came from.** The definition's default,
|
|
||||||
then the mesh-wide layer, then the node's — later wins, objects merge, lists replace. One consistent
|
|
||||||
configuration for every machine is the default plus the mesh-wide layer; one machine that differs says
|
|
||||||
so in its own layer and nothing else. Asked for a module's configuration on a machine, the mesh lists
|
|
||||||
every declared setting with its effective value and its source: *default*, *mesh*, or *node*.
|
|
||||||
|
|
||||||
**Changing later is changing one of three places, and the plan shows its reach before anything moves.**
|
|
||||||
A new default ships with the module's next version and reaches every assignment that does not override
|
|
||||||
it; a mesh-wide setting reaches every assignment of the module; a node's reaches one. The plan of a
|
|
||||||
change names each assignment whose effective value moves.
|
|
||||||
|
|
||||||
**A declared setting says where it lands.** Each names the file or files of its module that read it,
|
|
||||||
and reaches no other: a module that owns two mergeable files no longer has one flat map laid over both.
|
|
||||||
A file that names no setting takes none.
|
|
||||||
|
|
||||||
**A declared setting is the only kind accepted.** Setting a key the module does not declare is refused
|
|
||||||
when it is set, naming the declared keys, rather than reported when the machine is planned. The mesh's
|
|
||||||
own words — where a port, a directory or an operator's data is placed, how far an endpoint reaches —
|
|
||||||
are the mesh's to validate as they are today, and no module declares them. A module
|
|
||||||
that declares no settings keeps today's behaviour until it does; a catalogue test lists those modules,
|
|
||||||
and the list shrinks to empty before the implicit form is removed — design 27's rule for every retired
|
|
||||||
mechanism.
|
|
||||||
|
|
||||||
**A setting says what it costs: nothing, a reload, or a restart.** When a file changes, the host
|
|
||||||
applies the strongest cost among the settings whose values moved in it, so a key the software reads
|
|
||||||
only at start can no longer be written and never read. A setting that reaches a container's environment
|
|
||||||
costs that container being recreated, which the host already does when a container's specification
|
|
||||||
changes; it needs no declaration. A service's `reload-on` and `restart-on` keep
|
|
||||||
naming the files that are not settings — a generated roster, a credential.
|
|
||||||
|
|
||||||
**The container runtime is the first module to declare its settings** and the model for the rest:
|
|
||||||
its log rotation, keeping containers through a daemon restart, and its resolver are tunables, and
|
|
||||||
its trusted registries are what the mesh tells it.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- The console can show a module's settings as a form: what can be set, of what type, its default,
|
|
||||||
and where the current value came from. That is the surface the operator wants for changing a
|
|
||||||
default later.
|
|
||||||
- `settings set` can refuse an unknown key, so ADR 0046's rule is enforced where it was only stated.
|
|
||||||
- The resolver's upstreams, the runtime's log rotation, and other literals a definition carries
|
|
||||||
because it could not give them a default become declared tunables.
|
|
||||||
- **What got harder:** every module that takes settings must list them, and a mergeable file no
|
|
||||||
longer silently accepts a key its author did not foresee. A person who needs one adds it to the
|
|
||||||
definition, which is a new module version, not a setting.
|
|
||||||
- Issue 173's open question — a consumer checks nothing against a contract — is unchanged; this record
|
|
||||||
is the operator half of design 27's contract, not the provider half.
|
|
||||||
- Not decided here: the spelling (design 27); whether a node's layer may be narrowed to a single key
|
|
||||||
rather than replaced whole, as `settings set` does today.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
| Rule | Checked by |
|
|
||||||
|---|---|
|
|
||||||
| Every setting a module takes is declared | A parser test refusing a setting declaration without a type or meaning; a catalogue test listing modules with mergeable files or `${setting:}` and no declarations, which must be empty before the implicit form is removed |
|
|
||||||
| A tunable resolves to its default; an operator value without one is refused | Resolution tests: an unset tunable in a JSON file and in a text file both take the default; an unset setting with no default is refused naming it (0155's existing test) |
|
|
||||||
| A setting reaches only the files it names | A resolution test: a module with two mergeable files and a setting declared for one; the other file's content is unchanged by it (the case of issue 198) |
|
|
||||||
| An undeclared key is refused when set | A controller test: `settings set` with an undeclared key fails naming the declared keys, and nothing is stored |
|
|
||||||
| Every effective value names its source | A test listing a module's configuration on a node with one key from each of default, mesh and node |
|
|
||||||
| A change's reach is shown before it moves | A plan test: changing a mesh-wide setting names every assignment whose effective value moves and no other |
|
|
||||||
| The strongest cost applies | A host test: a file where a reload-cost key and a restart-cost key both moved restarts; a file where only reload-cost keys moved reloads |
|
|
||||||
| Live | The container runtime's module lists its settings with their sources on every machine, and a mesh-wide change to its log rotation reaches all four at the next push |
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md), [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)
|
|
||||||
- [Design 27 — a module requires, the mesh resolves](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md)
|
|
||||||
- Issues [110](../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md), [173](../04-ISSUES/173-a-modules-settings-reach-every-fact-it-contributes/00-report.md)
|
|
||||||
- mesh-controller `internal/catalogue/settings.go` (`settle`, `UnusedSettings`), `internal/catalogue/setting_into.go`
|
|
||||||
-105
@@ -1,105 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: proposed
|
|
||||||
date: 2026-10-01
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0161-what-deserves-a-seat.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 165. `container-runtime` is what a machine can run; that a runtime is running is its holder's health
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
A capability is a requirement a module places on a machine, detected by the host and renewed with
|
|
||||||
every report ([ADR 0161](0161-what-deserves-a-seat.md)). The host's `container-runtime` asks the
|
|
||||||
daemon for its version: *a running daemon, not an installed client*. It was made that way by
|
|
||||||
[issue 007](../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md), where an
|
|
||||||
installed package was believed to be a working service, and
|
|
||||||
[design 05](../03-DESIGN/01-to-be/05-the-node-host.md)'s table says the same: *a runtime is
|
|
||||||
running*. The installer's preflight borrows the same detector to wait for the runtime the
|
|
||||||
foundation bundle installs, so there is one answer to "is there a runtime here".
|
|
||||||
|
|
||||||
The mesh is now to have a module for the runtime itself — its packages, its configuration, its
|
|
||||||
service — on every machine ([ADR 0166](0166-the-container-runtime-is-a-node-seat-and-the-host-creates-containers-through-its-holder.md)).
|
|
||||||
That module cannot declare `container-runtime` as defined: it would require the very thing it
|
|
||||||
installs, the cycle [research 011](../01-RESEARCH/011-the-module-graph/cases.md)'s case 12 names
|
|
||||||
("something the mesh installs that then becomes a node capability"). The operator defined the word
|
|
||||||
for it: **`container-runtime` means the machine is able, at the kernel level, to install a runtime and
|
|
||||||
execute containers** — not that one is installed, and not that one is running.
|
|
||||||
|
|
||||||
The host already draws this line once. `seat` is hardware, a display server *could* run here;
|
|
||||||
`graphical-session` is state, one *is* running; the detector's own comment says "assignment needs the
|
|
||||||
first". A machine without a display has no seat however much software is installed, and a machine
|
|
||||||
with one has a seat before anything is.
|
|
||||||
|
|
||||||
Fifty-four catalogue modules declare `container-runtime` today, counted on the catalogue's main
|
|
||||||
branch on the day of deciding: every module that delivers a container. Each relies on the current
|
|
||||||
meaning to keep it off a machine with no running runtime.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
1. **Keep the meaning; let the runtime's module declare nothing.** Rejected: a module that installs
|
|
||||||
the runtime has requirements on the machine — the kernel features without which installing it is
|
|
||||||
pointless — and would state none of them. The cycle stays, only hidden.
|
|
||||||
2. **Two capabilities, "can run" and "is running".** Rejected: the second is made true by assigning a
|
|
||||||
module, so it is the module's state, not a fact of the machine; a capability the mesh itself
|
|
||||||
flips by its own assignment is case 12's cycle with an extra name.
|
|
||||||
3. **The capability is the kernel's; whether a runtime runs is the runtime module's health, and a
|
|
||||||
module that delivers a container needs the runtime's seat held.** Adopted.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**`container-runtime` is detected from what the kernel offers**, as `seat` is: the namespaces a
|
|
||||||
container needs, a control-group hierarchy the runtime can manage, and an overlay filesystem the
|
|
||||||
running kernel has or can load. Present when all three are; absent naming the missing one. Nothing is
|
|
||||||
run and no runtime is asked. The verdict's detail names what was found, not a runtime's version.
|
|
||||||
|
|
||||||
**"A runtime is running and answers" is one probe, owned by the host and used twice:** by the
|
|
||||||
installer's preflight, which waits for the runtime the foundation installs, and as the runtime
|
|
||||||
module's health. It asks the daemon, as issue 007 requires. The preflight stops borrowing the
|
|
||||||
capability's detector, and there is still one answer to "is a runtime running here".
|
|
||||||
|
|
||||||
**The runtime's module declares `container-runtime`**, with `package-manager`, `service-manager` and
|
|
||||||
`privileged`, like any module that manages machine software.
|
|
||||||
|
|
||||||
**A module that delivers a container needs the runtime seat held on its machine**, and is refused
|
|
||||||
otherwise, naming the seat and the modules that could hold it — the refusal design 27 already lists
|
|
||||||
for an unheld seat. That requirement is derived from the container resource and needs no manifest
|
|
||||||
field ([ADR 0166](0166-the-container-runtime-is-a-node-seat-and-the-host-creates-containers-through-its-holder.md)).
|
|
||||||
The fifty-four existing declarations of the capability stay valid and become redundant; a catalogue
|
|
||||||
test lists them, and they retire when the list is empty.
|
|
||||||
|
|
||||||
**The order is fixed, not preferred.** The detector changes only once the seat requirement is
|
|
||||||
enforced. In between, a machine with the kernel and no running runtime would read as able to run
|
|
||||||
every containerised module, which is issue 007 again.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- Design 05's capability table changes its `container-runtime` row from *a runtime is running* to
|
|
||||||
*the kernel can run containers*, and names the runtime module's health as where "running" is now
|
|
||||||
asked.
|
|
||||||
- The node listing stops showing the runtime's version beside the capability. The version moves to
|
|
||||||
the runtime module's health and its seat's verbs.
|
|
||||||
- A fresh machine with no runtime reads as able to run one, so it can be assigned the runtime's
|
|
||||||
module, which is what makes the mesh able to install the runtime instead of the bootstrap alone.
|
|
||||||
- **What got harder:** "is this machine running containers" is no longer one glance at the profile;
|
|
||||||
it is the runtime seat's holder and its health. The node's listing should show both side by side.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
| Rule | Checked by |
|
|
||||||
|---|---|
|
|
||||||
| The capability is the kernel's | Host detector tests over a fixture `/proc` and `/sys`: all three present → present; each one missing → absent naming it; no runtime binary on the fixture machine changes nothing |
|
|
||||||
| One probe asks whether a runtime runs | A host test that the preflight and the runtime module's health call the same probe, and that the probe fails against a stopped daemon with an installed client (issue 007's shape) |
|
|
||||||
| A containerised module needs the runtime seat held | A resolution test: a module with a container resource on a machine whose runtime seat is unheld is refused, naming the seat and its candidate holders |
|
|
||||||
| The order holds | The host release that changes the detector is gated on the controller release that enforces the seat requirement — stated in both changes' descriptions and checked at review |
|
|
||||||
| Live | Every machine's profile shows `container-runtime` present with the kernel's features as its detail; a machine with no runtime installed reads present too |
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0161](0161-what-deserves-a-seat.md) — the profile renewed by every report; a capability that names a dialect
|
|
||||||
- [ADR 0166](0166-the-container-runtime-is-a-node-seat-and-the-host-creates-containers-through-its-holder.md) — the seat and its holder
|
|
||||||
- [Issue 007](../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md), [research 011](../01-RESEARCH/011-the-module-graph/cases.md) cases 12–13
|
|
||||||
- [Design 05 — the node host](../03-DESIGN/01-to-be/05-the-node-host.md)
|
|
||||||
- mesh-host `internal/profile/detectors.go` (the runtime detector), `internal/profile/seat.go` (the hardware/state split), `internal/bootstrap/preflight.go` (the preflight that borrows it)
|
|
||||||
-161
@@ -1,161 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: proposed
|
|
||||||
date: 2026-10-01
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0161-what-deserves-a-seat.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 166. The container runtime is a node seat, and the host creates containers through its holder
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
Every container the mesh runs on a machine is created by the host, which looks for a runtime
|
|
||||||
(`docker info`, then `podman info`) and drives that runtime's command line itself: run, inspect,
|
|
||||||
remove, exec. Research 012 called this "detected rather than declared": the host takes over whatever
|
|
||||||
runtime it finds. Nothing in the mesh owns the runtime. Its package came from the foundation bundle
|
|
||||||
or was already on the machine. Its configuration file was written by hand, differs on each of the
|
|
||||||
four machines, and is also written into by two modules that are not the runtime's
|
|
||||||
([issue 190](../04-ISSUES/190-the-runtimes-configuration-is-written-by-modules-that-are-not-the-runtime/00-report.md)).
|
|
||||||
Its service is declared by those same two.
|
|
||||||
|
|
||||||
The operator set the direction:
|
|
||||||
|
|
||||||
- a module for the runtime, on every machine, owning "what is needed to run containers here": its
|
|
||||||
packages, its configuration and its service;
|
|
||||||
- that module holds a node seat for the runtime, so a second runtime (podman) can later compete
|
|
||||||
for the seat;
|
|
||||||
- the host stops speaking to the runtime directly and uses the seat's holder. The host stays the one
|
|
||||||
that decides, and the holder becomes the one that executes;
|
|
||||||
- every container on the machine is in scope, not only the mesh's. A development environment started
|
|
||||||
by hand, or a test database a tool runs, is legitimate. The host already calls these *strays*: 3,
|
|
||||||
8 and 25 on three of the machines on the day of deciding;
|
|
||||||
- the runtime's events and verbs are subjects on the bus, and the mesh's own interface is built on
|
|
||||||
them. The third-party interface run until now was removed by hand.
|
|
||||||
|
|
||||||
[ADR 0161](0161-what-deserves-a-seat.md)'s test for a seat is whether the mesh's own code finds it by
|
|
||||||
name. Here it does: the host would look up the holder on its own machine. A singular role of a module
|
|
||||||
held once per machine is a `node-*` seat ([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)),
|
|
||||||
in the controller's seed.
|
|
||||||
|
|
||||||
The constraint that decides most of this record is a cycle. The bus runs in containers. On the
|
|
||||||
broker's machine, the broker's own container is created by the host. A holder's code served from a
|
|
||||||
container cannot create the container that runs it. On a first machine, before the controller exists,
|
|
||||||
nothing holds anything.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
1. **The host calls the holder's verbs over the bus.** Rejected: with the broker down, no machine can
|
|
||||||
create any container, including the broker's. The mesh would be unable to restart its own
|
|
||||||
transport.
|
|
||||||
2. **The holder picks a dialect that the host speaks itself, as with the uplink.** Rejected: the
|
|
||||||
host would still drive the runtime, and the module would drive it too for every other caller.
|
|
||||||
That is two programs speaking to one daemon, and they come to disagree about the same machine
|
|
||||||
(the installer's preflight already exists to avoid this).
|
|
||||||
3. **The holder's code runs as a supervised process on the machine and serves the seat's verbs
|
|
||||||
twice: locally to the host, on the bus to everyone else.** Adopted.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**`node-container-runtime` is a seat of the mesh's own, node-scoped,** in the controller's seed under
|
|
||||||
this record. It delivers no provision; what it carries is its role's protocol: verbs its holder must
|
|
||||||
serve ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)) and events its holder emits
|
|
||||||
([ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md)). The runtime's module, `docker`, claims
|
|
||||||
it and is assigned to every machine. A podman module may claim it later; one machine runs one.
|
|
||||||
|
|
||||||
**The seat's verbs cover every container on the machine:** list, inspect, logs, stats, start, stop,
|
|
||||||
restart, create and remove. A mesh-held container is marked by the host's label and says which
|
|
||||||
assignment holds it. **A container the runtime runs can be root on the machine** — privileged, a host
|
|
||||||
path mounted, the host's network or process namespace, the runtime's own socket — so a caller other
|
|
||||||
than the host may not create one that is any of these; only a declaration the mesh composed may ask
|
|
||||||
for them. And the verbs that change anything are granted by name, never by a wildcard: a grant of
|
|
||||||
every tool (the console's today) reaches the reading verbs only. Issue 193 is what a verb that trusts
|
|
||||||
its caller costs. **Creating or removing a mesh-held container is the host's alone.** Any other
|
|
||||||
caller is refused naming the assignment, because the host would undo it at its next apply. Starting,
|
|
||||||
stopping or restarting one is allowed, and the answer says the host will restore what its
|
|
||||||
declaration says. A container the mesh does not hold is the caller's to do anything with.
|
|
||||||
|
|
||||||
**The seat's events are the runtime's own** — a container created, started, stopped, died, removed,
|
|
||||||
its health changed. They are emitted on the seat's subjects, so every holder emits the same events and
|
|
||||||
no reader depends on which runtime holds the seat. As
|
|
||||||
[ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
|
||||||
decides, the subjects are issued by the controller, not composed by the module.
|
|
||||||
|
|
||||||
**The holder's code is a supervised process, not a container** ([ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)).
|
|
||||||
A runtime cannot be run by the thing it runs. The process serves the seat's verbs on the bus to the
|
|
||||||
console, to tools and to the mesh's interface. The same verbs are served on a local socket on the
|
|
||||||
machine, which only the host may use. **The host creates, inspects and removes its containers
|
|
||||||
through that socket and nothing else.** If the holder does not answer, the host creates nothing. It
|
|
||||||
says so in its report, naming the seat. It never falls back to the command line.
|
|
||||||
|
|
||||||
**A container needs the seat held on its machine.** An assignment that delivers a container on a
|
|
||||||
machine whose runtime seat is unheld is refused, naming the seat and its candidates
|
|
||||||
([ADR 0165](0165-container-runtime-is-what-a-machine-can-run-and-a-running-runtime-is-its-holders-health.md)).
|
|
||||||
Mounting the runtime's socket into a container is granted by the seat, not by the capability. The
|
|
||||||
socket's path is the holder's to state, because podman's is not docker's.
|
|
||||||
|
|
||||||
**The runtime module owns the runtime's configuration.** Its settings are declared with defaults
|
|
||||||
([ADR 0164](0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md)):
|
|
||||||
the resolver containers use, the registries it trusts, log rotation, and keeping containers through a
|
|
||||||
daemon restart. The module is given the resolver's address and the mesh's registry as values; no other
|
|
||||||
module writes the runtime's file or declares its service.
|
|
||||||
|
|
||||||
**The first machine is bootstrapped with the holder, and adopted afterwards.** The foundation bundle
|
|
||||||
already installs the runtime's package and service. It also carries the holder's process, delivered as
|
|
||||||
a binary the way the host is ([ADR 0142](0142-the-mesh-delivers-its-own-components-as-binaries.md)).
|
|
||||||
When the runtime module is assigned, it adopts what the bundle made, as the store and broker modules
|
|
||||||
adopt theirs ([ADR 0078](0078-the-store-and-broker-are-modules.md)).
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **The migration on the running mesh has a fixed order:**
|
|
||||||
1. Each machine's hand-written configuration is read, because the module's defaults replace what
|
|
||||||
differs.
|
|
||||||
2. In one push per machine: the resolver module and the private network stop writing the
|
|
||||||
runtime's file ([issue 190](../04-ISSUES/190-the-runtimes-configuration-is-written-by-modules-that-are-not-the-runtime/00-report.md)),
|
|
||||||
and the runtime module is assigned and adopts the runtime, its file and its service. Split in
|
|
||||||
two, either the controller refuses two modules declaring one path, or a machine is left with
|
|
||||||
nothing setting `dns` and `live-restore`.
|
|
||||||
3. The controller seeds the seat and enforces the container requirement.
|
|
||||||
4. The host releases the version that uses the holder.
|
|
||||||
5. The host's command-line path is removed in the release after every machine's holder answers.
|
|
||||||
Until then, the host reports per machine which path it used.
|
|
||||||
- **Every container the host makes depends on the holder's process.** A crash-looping holder stops
|
|
||||||
new containers on its machine. Running containers are unaffected. The host's report names the cause.
|
|
||||||
- The process form of a module's own code must serve tools on the live mesh before this ships. Only the
|
|
||||||
showcase declares it, and [issue 117](../04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/01-diagnosis.md)
|
|
||||||
found the showcase's tools declared in a form nothing runs. The runtime module is the first whose
|
|
||||||
tools cannot fall back to a container.
|
|
||||||
- A user interface subscribing to events directly does not exist. Today a reader of events is a module
|
|
||||||
that consumes them. The mesh's container view is a module, or waits for that path.
|
|
||||||
- [ADR 0005](0005-the-node-host.md) ("a container runtime is detected, not chosen") and
|
|
||||||
[ADR 0006](0006-the-substrate-and-the-control-plane.md)'s matching line describe the mechanism this replaces: on
|
|
||||||
acceptance, each gets a dated note saying the runtime is now a seat's holder, as the decision
|
|
||||||
records' rule for a moved mechanism requires. Design 05 and design 26 are amended after acceptance.
|
|
||||||
- The operator's decision to remove the third-party interface by hand needs no mechanism. No
|
|
||||||
module-retires-module rule is introduced.
|
|
||||||
- **What got harder:** the host gains a dependency it did not have, and a first machine's bundle gains
|
|
||||||
a component. The direct path was simpler and is what makes a runtime a black box to the rest of the
|
|
||||||
mesh.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
| Rule | Checked by |
|
|
||||||
|---|---|
|
|
||||||
| The seat is the mesh's own, node-scoped, with its verbs and events | A catalogue test on the default seats; registration refuses a claimant that does not serve every verb (design 33's existing check) |
|
|
||||||
| Creating or removing a mesh-held container is the host's alone | A test of the runtime module's verbs: create or remove of a container carrying the host's label, from any caller but the host's socket, is refused naming the assignment; the same verbs on an unlabelled container succeed |
|
|
||||||
| No caller but the host creates a container that is root on the machine | A test of `create` from the bus: privileged, a host path, the host's namespaces and the runtime's socket are each refused; the same request on the host's socket is accepted. A broker test: a grant of every tool does not reach a changing verb |
|
|
||||||
| The host uses the holder and never the command line | A host test with a fake holder on the local socket: every container operation goes to it, and with the holder absent the apply creates nothing and reports the seat; after step 5, the host carries no command-line runtime code (checked by build: the package is gone) |
|
|
||||||
| A container needs the seat held | A resolution test refusing a containerised assignment on a machine with the seat unheld, naming the seat |
|
|
||||||
| Socket mounts are granted by the seat | A catalogue test: a module mounting the runtime's socket on a machine whose holder states a different path is refused |
|
|
||||||
| No other module writes the runtime's file | The existing collision check, once the private network's computed resources are inside it ([issue 190](../04-ISSUES/190-the-runtimes-configuration-is-written-by-modules-that-are-not-the-runtime/00-report.md)) |
|
|
||||||
| Live | `seats` lists `node-container-runtime` held on every machine; the node listing shows each machine's containers, strays included, from the seat's `list` verb; a container started by hand appears as an event on the bus |
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md), [ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md), [ADR 0161](0161-what-deserves-a-seat.md)
|
|
||||||
- [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md), [ADR 0142](0142-the-mesh-delivers-its-own-components-as-binaries.md), [ADR 0078](0078-the-store-and-broker-are-modules.md), [ADR 0005](0005-the-node-host.md)
|
|
||||||
- [ADR 0164](0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md), [ADR 0165](0165-container-runtime-is-what-a-machine-can-run-and-a-running-runtime-is-its-holders-health.md), [issue 190](../04-ISSUES/190-the-runtimes-configuration-is-written-by-modules-that-are-not-the-runtime/00-report.md)
|
|
||||||
- [Design 26 — the seats](../03-DESIGN/01-to-be/26-the-seats.md), [design 33 — the tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)
|
|
||||||
- mesh-host `internal/apply/apply.go` (the runtime lookup and the command line it drives)
|
|
||||||
@@ -113,6 +113,22 @@ That is a difference a take shows, not a fault, and is decided when it bites.
|
|||||||
| `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 |
|
| `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 |
|
| 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
|
## 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)
|
- [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)
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0004-a-node-and-how-it-joins.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 169. A machine joins through the tunnel, and the bus is never public
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The bus is the one channel every machine depends on: enrolment, every declaration, every tool. The
|
||||||
|
`nats` module declares it reachable from the mesh only. The controller still opens it to the whole
|
||||||
|
internet on the machine that runs it, as a *foundation* port that no module declares and nothing may
|
||||||
|
close ([issue 051](../04-ISSUES/051-the-mesh-cannot-update-what-it-depends-on/00-report.md)).
|
||||||
|
The reason is joining. [ADR 0004](0004-a-node-and-how-it-joins.md) has a new machine enrol over the bus
|
||||||
|
**before** it has a tunnel. [ADR 0007](0007-connectivity.md) states it as a requirement: the node
|
||||||
|
running the broker must be reachable from wherever nodes are, at a stable address.
|
||||||
|
|
||||||
|
So the bus listens on the internet permanently, for an event that happens a few times a year. A
|
||||||
|
sweep of every machine on 2026-10-02 found no client using the public path. Every connection arrives
|
||||||
|
over the tunnel or from the machine itself. The join token does not use it either: it carries the
|
||||||
|
controller's configured broker address, a mesh name with the old broker's port.
|
||||||
|
|
||||||
|
ADR 0004 already says what a joining machine needs: *an identity, an address, and one peer to reach*.
|
||||||
|
The tunnel can be that peer, if the hub knows the new machine's key before the machine first knocks.
|
||||||
|
WireGuard answers nothing to a key it does not know, which is why the tunnel's own port is safe to
|
||||||
|
leave open where the bus's is not.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep the bus public.** It is authenticated and encrypted, but every exposure of it, and of the
|
||||||
|
server behind it, is exposure of the one thing everything depends on.
|
||||||
|
2. **Open the bus publicly only while a join token is live.** Small, and the hub is open only during a
|
||||||
|
join window. But the window is real, the rule is about time rather than about who may reach the
|
||||||
|
bus, and the opening and closing are pushes that can fail between them.
|
||||||
|
3. **The controller makes the new machine's tunnel key and puts it in the token.** One step for the
|
||||||
|
operator, but the private half leaves a machine it does not belong to. ADR 0004 refuses that for
|
||||||
|
every key a node holds.
|
||||||
|
4. **The machine makes its key first, and the token is issued for it.** The machine prints the public
|
||||||
|
half of its tunnel key. The operator issues the token for that key. The controller gives the
|
||||||
|
machine its address and adds it as a peer on the hub. The token carries the hub's tunnel endpoint
|
||||||
|
and key, the machine's address, and the bus's address on the private network. The machine brings
|
||||||
|
up its tunnel and enrols over it.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Option 4.**
|
||||||
|
|
||||||
|
- **A machine makes its own tunnel key before it has a token**, and prints the public half. The private
|
||||||
|
half never leaves it, as ADR 0004 says of every key a node holds.
|
||||||
|
- **A token is issued for a tunnel key.** Issuing it assigns the machine's address on the private
|
||||||
|
network, records the key, and makes the machine a peer of the hub. The hub is sent that before the
|
||||||
|
token is shown, so the tunnel answers the moment the machine first uses it.
|
||||||
|
- **The token carries the one peer.** It adds the hub's tunnel endpoint and public key and the
|
||||||
|
machine's own address. **Where** becomes the bus's address on the private network, which needs no
|
||||||
|
name resolution.
|
||||||
|
- **The machine joins through the tunnel.** It brings the tunnel up from the token alone, then enrols
|
||||||
|
over it exactly as before. The enrolment checks that the key it is offered is the one the token was
|
||||||
|
issued for.
|
||||||
|
- **The bus is never public.** It is no longer a foundation port. Its reach is what the `nats` module
|
||||||
|
declares: the mesh. The tunnel's port stays open, as the one way in.
|
||||||
|
|
||||||
|
This changes three things earlier records say. ADR 0004's *where* is the bus's private address, and the
|
||||||
|
token carries the peer. ADR 0007's requirement that the broker be reachable from wherever nodes are
|
||||||
|
becomes: **the hub's tunnel is**. Issue 051's broker port stops being a foundation port.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Joining is two commands on the new machine, with the token issued between them. A token issued for
|
||||||
|
the wrong key gives a tunnel that never answers, and the machine says so rather than timing out at
|
||||||
|
the bus.
|
||||||
|
- An unused token leaves a peer on the hub until it expires. Expiry removes it, the same way it voids
|
||||||
|
the secret.
|
||||||
|
- A machine already in the mesh is unaffected: it reaches the bus over its tunnel today.
|
||||||
|
- The genesis machine, the first one, raises the bus on itself and needs no tunnel to reach it.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A token is refused without a tunnel key, and carries the hub's peer and the machine's address | a controller test |
|
||||||
|
| Issuing a token makes the machine a peer of the hub before the token is shown | a controller test over the hub's composed tunnel |
|
||||||
|
| An expired, unused token's peer is gone from the hub | a controller test |
|
||||||
|
| Enrolment refuses a tunnel key other than the one the token was issued for | a controller test |
|
||||||
|
| No machine's filter opens the bus to anywhere | a controller test over the composed filter, and the live sweep from outside the mesh |
|
||||||
|
| A new machine joins from outside the hub's network with the bus closed to it | the lab, then by hand |
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 170. The firewall seat serves its verbs, and a foreign rule set is removed through one of them
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md) made the mesh say truthfully
|
||||||
|
what filters a converged machine, and left the removal of what it did not write to the operator's
|
||||||
|
hand. The first time that hand was needed — two machines, five rule sets a predecessor and a
|
||||||
|
retired front end had left — there was no mesh way to lend it: the packet filter is a seat
|
||||||
|
([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)), a seat's
|
||||||
|
holder serves its verbs ([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md),
|
||||||
|
[ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)), and the
|
||||||
|
firewall seat declared none. The only remaining path was a shell on the machine, which is the path
|
||||||
|
the mesh exists to replace, and which the operator's own tooling rightly refused to an agent.
|
||||||
|
|
||||||
|
A seat's verbs are the contract every holder implements, whatever filter it speaks. What a person
|
||||||
|
asks a machine's packet filter is the same whether nftables, a front end or a legacy filter answers:
|
||||||
|
what are the rules, reload the mesh's own, remove this thing the mesh did not write. What differs by
|
||||||
|
filter is the holder's own business and may be its own tools beside the seat's.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. The `node-packet-filter` seat serves three verbs**, and a module that claims it serves all
|
||||||
|
three or is refused the claim, as with every seat:
|
||||||
|
|
||||||
|
- `rules` — the packet filter as the machine enforces it now: the nftables ruleset, and the legacy
|
||||||
|
filter's listings where that tool exists; narrowed to one table or chain when asked. Read-only.
|
||||||
|
- `reload` — load the mesh's own filter again from the file the mesh writes, and answer with the
|
||||||
|
mesh's table as loaded. The holder's own act on the holder's own rules.
|
||||||
|
- `remove` — remove one rule set the mesh did not write, named exactly as the host reports it under
|
||||||
|
ADR 0168 (`chain HAL-MESH-ONLY (iptables-legacy)`, `table ip6 filter, chain DOCKER-USER`), and
|
||||||
|
answer with what was done. It refuses the mesh's own tables, the container runtime's own chains,
|
||||||
|
a built-in chain other than the runtime's user chain, and any chain of a found firewall that is
|
||||||
|
in force. The runtime's user chain is emptied back to its one return; another chain loses the
|
||||||
|
jumps into it, is flushed and deleted; a table of the machine's own is deleted whole. Each is an
|
||||||
|
operator's act, by name, on one thing the mesh reported — never a flush, never a rule the mesh
|
||||||
|
itself marked.
|
||||||
|
|
||||||
|
**2. A holder may serve its own tools beside the seat's.** The nftables module keeps its reading of
|
||||||
|
the mesh's table as its own tool, and a holder speaking a filter with specifics of its own may add
|
||||||
|
tools for them; the seat's three are what every holder owes.
|
||||||
|
|
||||||
|
**3. A container may ask for a capability.** Serving `remove` and `reload` needs the machine's
|
||||||
|
network namespace and the right to change its packet filter; a holder's runtime declares
|
||||||
|
`capabilities: ["NET_ADMIN"]` on its container and runs on the machine's network. The host grants
|
||||||
|
exactly the capabilities declared, names them in the container's spec so a change recreates it, and
|
||||||
|
refuses a name that is not a capability's. A privileged container stays undeclarable.
|
||||||
|
|
||||||
|
**4. ADR 0168's "by hand" is read as "by the operator, through the seat".** Removing what the mesh
|
||||||
|
reports as *other* is still the operator's act and is still never the mesh's own doing; the verb is
|
||||||
|
how the act reaches the machine, recorded on the bus like every other, instead of a shell.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The seat's row gains the three verbs; a mesh that already runs widens its row at the next
|
||||||
|
controller start. The nftables module claims them and gains a runtime — a tool server with the
|
||||||
|
packet filter's tools in its image, on the machine's network, with `NET_ADMIN`.
|
||||||
|
- The host's container vocabulary grows by `capabilities`; an older host refuses a declaration that
|
||||||
|
carries it, so the host rolls before the module.
|
||||||
|
- The two machines of this mesh that ADR 0168 found not filtered by the mesh alone are cleaned
|
||||||
|
through `remove`, and read *the mesh alone* afterwards; `status` returns to well without a hand on
|
||||||
|
either machine.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| The seat declares the three verbs; a claim that serves fewer is refused by name | the catalogue's seat tests |
|
||||||
|
| `remove` refuses the mesh's tables, the runtime's chains, a built-in chain and an active front end's chains, and removes a user chain with its jumps, empties the user chain, deletes an own table | the module's tests over a fake command runner, with the shapes the host reported live |
|
||||||
|
| A container's capabilities reach the runtime and its spec; an unknown name is refused | host tests |
|
||||||
|
| Live | `node-packet-filter.remove@<node>` on the home server and the control node; `node show` reads *the mesh alone* on both; `status` is well |
|
||||||
|
|
||||||
|
## Built and proven live, 2026-10-02
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-02.** The decision stands; these are the facts of its building.
|
||||||
|
> Written as 0169 for three hours and renumbered to 0170: another record took 0169 on main first,
|
||||||
|
> and the check that refuses a shared number covered issues only (now records too).
|
||||||
|
|
||||||
|
Built in mesh-host 68 (`capabilities` on a container), mesh-controller 212 (the seat's three verbs)
|
||||||
|
and 213 (the filter file a module names under `filtering.into` counts as declared for a mount — the
|
||||||
|
module's first build was refused without it), mesh-catalog 216 (the nftables module's runtime and
|
||||||
|
verbs) and mesh-tools 27 (the console lists a node-scoped seat's verbs with their scope and carries the
|
||||||
|
machine; before it, the verbs were live on four machines and unreachable from the console —
|
||||||
|
[issue 199](../04-ISSUES/199-a-node-scoped-seats-verb-could-not-be-called-through-the-console/00-report.md)).
|
||||||
|
Each machine's holder was issued its bus account with `mesh-controller.issue`, the broker node pushed
|
||||||
|
first. At 12:46Z the five rule sets ADR 0168 had named were removed through
|
||||||
|
`node-packet-filter.remove`, three on the home server and two on the control node, each answering
|
||||||
|
with the commands it ran; the next report read *the mesh alone* on all four machines and `status`
|
||||||
|
listed nothing under `filtered`. The live row is read. What it cost on the way is
|
||||||
|
[issue 200](../04-ISSUES/200-the-controllers-answer-to-the-console-is-refused-by-the-bus/00-report.md).
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
|
||||||
|
- [Design 33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md), [Design 08 — Connectivity](../03-DESIGN/01-to-be/08-connectivity.md)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0016-the-lab.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 172. The lab is a module, and runs a bed when the mesh asks
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The lab raises virtual machines and runs the mesh on them, end to end, before a change reaches a real
|
||||||
|
machine ([ADR 0016](0016-the-lab.md)). It runs on one machine of the mesh, the one with the
|
||||||
|
virtualisation it needs. Until now the only way to start a bed there was to sign in to that machine and
|
||||||
|
run the lab's command line by hand, with a dozen environment variables pointing at sibling checkouts.
|
||||||
|
|
||||||
|
Nothing in the mesh could ask for it. An agent working through the mesh's own tools could build,
|
||||||
|
merge and push a change, and could not prove it in the lab first. The operator's direction on
|
||||||
|
2026-10-02: work on another machine goes through a mesh tool, not a shell on it.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep the lab a command line on one machine.** Every run is a person, or an agent with a shell on
|
||||||
|
that machine, outside the mesh.
|
||||||
|
2. **The lab is a module.** Assigned to the machine that can run it, serving tools that run a bed
|
||||||
|
against named branches and say how it went.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Option 2.**
|
||||||
|
|
||||||
|
- **A `lab` module, assigned where the lab can run**, serves five tools: whether this machine can run
|
||||||
|
beds, run beds against a branch per repository, a run's state, its log, and stopping it.
|
||||||
|
- **A run is the lab's own suite**, against fresh checkouts of the named branches from the mesh's forge,
|
||||||
|
side by side as the lab expects them. It builds what the beds place from those checkouts, as the suite
|
||||||
|
already does. It answers at once with an id, like a build: a bed takes minutes, and a call does not.
|
||||||
|
- **Only branches on the forge are run**, never code handed to the tool. What a run tested is what the
|
||||||
|
forge holds at the commit it names.
|
||||||
|
- **The lab is reached over the mesh only.** Its tools travel the bus, and the module opens no port.
|
||||||
|
- **No grant beyond the mesh's own.** Running a bed is root on the lab's machine, but anyone who can call
|
||||||
|
the mesh's tools can already do worse. The operator's judgement on 2026-10-02.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- An agent proves a change in the lab through the mesh, the same way it builds and pushes one.
|
||||||
|
- The lab's machine carries a module whose runtime holds the virtualisation's and the container
|
||||||
|
runtime's sockets, and a toolchain to build the mesh with.
|
||||||
|
- A run's checkouts are its own, so two runs never build from each other's tree. Old ones are removed
|
||||||
|
when their run ends.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A run checks out exactly the named branches, and reports the commits it tested | the module's tests over a forge fixture, and each run's answer |
|
||||||
|
| A run answers at once, and its state and log follow it to the end | by hand, the first run |
|
||||||
|
| The module opens no port | the composed filter of the lab's machine |
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0040-what-a-module-is.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 173. The operator's machine is the mesh's, and a module is whatever it declares
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0040](0040-what-a-module-is.md) says a module is *one self-contained piece of software the
|
||||||
|
mesh installs and manages*, and every example it gives is a service: a database, an analytics
|
||||||
|
server, a forge. The catalogue followed the examples. Of the predecessor's 34 modules on one
|
||||||
|
workstation, 28 are the operator's environment — a login manager, a window manager with 88 files
|
||||||
|
and four flavors, a shell, a terminal, a launcher, an audio setup, scripts — and the migration
|
||||||
|
scoped all 28 out as *the workstation's own environment*, to be managed by nobody
|
||||||
|
([research 018](../01-RESEARCH/018-the-operators-machine-as-modules/02-what-exists-and-what-is-missing.md)).
|
||||||
|
Since the predecessor retired, nobody is exactly who manages them: a fix is a hand edit that
|
||||||
|
nothing records and nothing regenerates.
|
||||||
|
|
||||||
|
[To-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) reached under the home for
|
||||||
|
one directory and drew a boundary inside it. The operator's statement is wider: *the mesh manages
|
||||||
|
my entire machine, all four of them, as far as it makes sense* — system folders and the home
|
||||||
|
alike, the servers and the workstations from the same catalogue. And the operator refused a
|
||||||
|
distinction this effort first drew between modules that ship code and modules that ship only
|
||||||
|
declarations: *a module can have some tools, a seat implementation, some containers, a unit, a
|
||||||
|
binary, some config files — one of these, or all, or two.*
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep 0040's reading and manage the environment outside the catalogue** — dotfiles in a
|
||||||
|
repository, a script that places them. Rejected: that is the predecessor's first two days, the
|
||||||
|
origin of every inherited shape [as-is 10](../03-DESIGN/00-as-is/10-module-catalogue.md)
|
||||||
|
documents, and it puts the one thing a person looks at outside the one mechanism that is
|
||||||
|
checked.
|
||||||
|
2. **Add a second kind of module for configuration** — a "config module" with files and no
|
||||||
|
process. Rejected by the operator: a kind is a distinction the manifest already makes by what
|
||||||
|
it declares, and a second kind is a second set of rules to keep in step.
|
||||||
|
3. **One definition: a module is one managed thing, described by what it declares.** Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. Everything configurable on a node is declared by a module.** Services, and equally the login
|
||||||
|
manager, the display server, the window manager, the shell, the terminal, the launcher, the
|
||||||
|
notifier, the audio setup, the boot images, the package manager's configuration, the agent at the
|
||||||
|
terminal, and a folder a person works in. The test is *can it be configured on a machine*; if it
|
||||||
|
can, some module owns it. What no module declares is found and left alone, as adoption already
|
||||||
|
says of a machine ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
|
||||||
|
|
||||||
|
**2. A module is whatever it declares, and there are no kinds of module.** A package, files, a
|
||||||
|
container, a unit, a binary, a seat claim, tools — any one, or all. 0040's *one self-contained piece
|
||||||
|
of software* stands; its examples were services, and that was the whole of the bias. A downloads
|
||||||
|
folder with a process that tidies it, backs it up and answers questions about it is a piece of
|
||||||
|
software by 0040's own test, and so is a shell that is a package, three files and a seat.
|
||||||
|
|
||||||
|
**3. The home has no boundary of its own.** A file under the operator's home is placed and owned
|
||||||
|
the way [to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §2 built it: by a
|
||||||
|
module, resolved against the account's home, owned by the account. Which files are the mesh's is
|
||||||
|
decided by what modules declare, not by a line drawn through a directory. A person's documents,
|
||||||
|
projects and history are data under [ADR 0051](0051-shared-data-is-the-operators.md) and no module
|
||||||
|
declares them.
|
||||||
|
|
||||||
|
**4. One module ships one default configuration.** No flavors. What differed between the
|
||||||
|
predecessor's four flavors of one desktop module is what [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)
|
||||||
|
is for.
|
||||||
|
|
||||||
|
**5. Servers and workstations take the same catalogue.** A module declares what it needs; a
|
||||||
|
machine reports what it has; assignment refuses by name
|
||||||
|
([ADR 0161](0161-what-deserves-a-seat.md) §3). The shell, the prompt, git and the agent are universal.
|
||||||
|
A display server needs a graphical session; a window manager needs the display server held. Nothing
|
||||||
|
in a manifest says *workstation*.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The catalogue grows by a family of modules that run no service. Each is still built,
|
||||||
|
registered, assigned, pushed and reported like every other, and `status` says whether a
|
||||||
|
machine has applied them.
|
||||||
|
- The account fact becomes load-bearing for every node a person uses. Today it is empty on all
|
||||||
|
four node records of this mesh; stating it is the first step of the build.
|
||||||
|
- A module that *installs* a thing is distinct from a module that *holds its role*: zsh, fish and
|
||||||
|
bash may all be installed, and one holds the login shell
|
||||||
|
([ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)).
|
||||||
|
- The host's `package` shape drives the distribution's package manager only. A module whose
|
||||||
|
package is outside the distribution's repositories — the login manager in use is one — needs
|
||||||
|
either an official package or a shape the host does not have. Recorded as a gap, not decided.
|
||||||
|
- The predecessor's hooks go. What they did becomes declared state the host applies, or a verb a
|
||||||
|
seat serves ([ADR 0177](0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)).
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A manifest with no container, no unit and no binary registers and resolves like any other | the catalogue's registration tests, with a package-and-files manifest |
|
||||||
|
| A file resource under the home resolves against the account and is owned by it | the controller's composition tests (to-be 29 §2, built) |
|
||||||
|
| A home-scoped module is refused on a node with no account, naming the fact | the same tests |
|
||||||
|
| A module needing a capability the machine lacks is refused by name | the resolver's tests (ADR 0161 §3) |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md), documents
|
||||||
|
01 and 02 — the behaviour wanted and the inventory measured.
|
||||||
|
- [ADR 0040](0040-what-a-module-is.md), [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md),
|
||||||
|
[ADR 0051](0051-shared-data-is-the-operators.md), [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)
|
||||||
|
- [To-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) — the account and the home
|
||||||
|
as a placement root, built; the records for them are proposed in an open change.
|
||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
topic: building it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0011-managed-files-are-generated-never-edited.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 174. A node varies a module through settings and kept regions, never through an edit
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0011](0011-managed-files-are-generated-never-edited.md) says a managed file is derived and an
|
||||||
|
edit to it is overwritten without warning. The predecessor said the same and then undid it twice:
|
||||||
|
a `merge` strategy that adopted disk drift back into its database, so a local edit became the
|
||||||
|
record; and a theming layer of about 90 environment variables substituted into templates at sync
|
||||||
|
time, with tools to list and set them, so that *nearly every value was a variable* — a second
|
||||||
|
configuration language laid over the first.
|
||||||
|
|
||||||
|
The operator wants both the variation and the rule. One window-manager module with one default
|
||||||
|
configuration, and each node tweaking it; and the file carrying the wanted value rather than a
|
||||||
|
variable the file reads. Two mechanisms already exist for exactly this: a **setting**, declared by
|
||||||
|
the module and set per mesh or per node, rendered at composition
|
||||||
|
(`${setting:…}` is live in the resolver's manifest); and a **kept region**, a block in a file the
|
||||||
|
mesh writes *into* where the operator's own lines survive every push
|
||||||
|
([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), used by the ssh-client module
|
||||||
|
for the operator's own `Host` blocks).
|
||||||
|
|
||||||
|
What stands in the way is [issue 168](../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md):
|
||||||
|
a setting today reaches every mergeable file and every contribution of its module. Ninety theme
|
||||||
|
knobs on that mechanism would reach ninety files. The record that fixes it — a setting declared
|
||||||
|
with its type, meaning, default and the file it lands in — is proposed in an open change alongside
|
||||||
|
the container-runtime records.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Carry the predecessor's merge strategy.** A local edit is adopted into the node's layer.
|
||||||
|
Rejected: two writers and no arbiter, which is the option 0011 removed, and the reason a
|
||||||
|
`/model` choice was silently reverted on every node for weeks before anyone found the cause.
|
||||||
|
2. **Carry the environment-variable theming.** Rejected by the operator: the value belongs in
|
||||||
|
the file; a variable the file reads is a second place for the same fact.
|
||||||
|
3. **A per-node file override** — a whole file replaced for one node. Rejected: it is a flavor
|
||||||
|
under another name, and a module update then misses that node entirely.
|
||||||
|
4. **Settings rendered into the file, and kept regions, and nothing else.** Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**A node varies a module in exactly two ways.**
|
||||||
|
|
||||||
|
- **A setting.** Declared by the module with a default, set for the mesh or for one node, rendered
|
||||||
|
into the file at composition. The value is in the file. Asked, the mesh lists every setting
|
||||||
|
with its effective value and where it came from.
|
||||||
|
- **A kept region.** A marked block in a file the mesh writes into, in which the operator's own
|
||||||
|
lines are kept across every push and given back when the module goes (ADR 0102).
|
||||||
|
|
||||||
|
**An edit outside a kept region is overwritten, as ADR 0011 says, and never adopted.** Nothing
|
||||||
|
reads a managed file back into the record.
|
||||||
|
|
||||||
|
**The predecessor's theme knobs become settings** of the modules whose files they render — the
|
||||||
|
window manager's colours are the window manager's settings, the bar's are the bar's — each
|
||||||
|
landing in the file that reads it and no other.
|
||||||
|
|
||||||
|
**Issue 168 is fixed before any environment module declares a setting.** A setting must name the
|
||||||
|
file it lands in; until that ships, the environment modules carry their defaults in their files
|
||||||
|
and no settings.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- No flavors, no per-node file copies, no environment layer. A module's definition is one set of
|
||||||
|
files; a node's difference is data in its layer, visible by asking.
|
||||||
|
- The settings record proposed alongside the container-runtime records is on the critical path
|
||||||
|
of every module with a knob, and this record depends on it shipping as proposed.
|
||||||
|
- A kept region is the only place a person edits a managed file, and the file says where it is.
|
||||||
|
The operator's own prompt customisations, aliases and window rules live there.
|
||||||
|
- What got harder: a change that is neither a setting the module declared nor the operator's own
|
||||||
|
lines has no home, and is refused by the mechanism rather than silently kept. That is the point.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A setting reaches only the file its declaration names | the controller's settings tests, once the proposed record ships; issue 168 closes on it |
|
||||||
|
| A kept region survives a push with its content and is given back on undeclare | the host's write-into tests (ADR 0102), with a region declared by an environment module |
|
||||||
|
| An edit outside a region does not survive a push | the same tests, asserting the file equals the composed content outside the region |
|
||||||
|
| Every effective value names its source | `mesh-controller.settings` and the module's own `show-config` tool |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/01-the-intended-behaviour.md) §"One default, varied by settings, never by edits"
|
||||||
|
- [ADR 0011](0011-managed-files-are-generated-never-edited.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md),
|
||||||
|
[issue 168](../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 175. One tool runtime per node serves every module's tools, on the host side
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
A module's tools are code the module wrote, one function behind each verb, served on the subjects
|
||||||
|
the controller issues in the module's membership
|
||||||
|
([ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)).
|
||||||
|
What *runs* that code is [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md):
|
||||||
|
a supervised process per module under the module's own account, and in the catalogue as built,
|
||||||
|
that process is a container per module per node, built on the tool runtime's base image.
|
||||||
|
|
||||||
|
Measured on the live mesh ([research 018](../01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md)):
|
||||||
|
67 module tools, each served from its module's container; the packet-filter seat's three verbs
|
||||||
|
served by a container with `NET_ADMIN` on every one of four machines, for a module that is
|
||||||
|
otherwise a package, three files and a service; and the console, a container per node, calling
|
||||||
|
everything and serving nothing. The operator's environment adds a dozen modules of the
|
||||||
|
packet-filter shape, and the operator's judgement is plain: *I would never run MCP tools inside
|
||||||
|
a container; that is a very bad design.* And: *I don't care about permissions or account per
|
||||||
|
module, that just complicates things for no good reason. Just a node-level tool executor. If a
|
||||||
|
command needs root, that's the module's concern.*
|
||||||
|
|
||||||
|
The tool runtime itself was written for this. Its own description: *the per-node process that
|
||||||
|
makes a module's tools actually serve — imports the assigned modules' compiled tool entrypoints,
|
||||||
|
each of which registers its tools as it loads; on a node the host resolves the list and starts it
|
||||||
|
like any other supervised workload.* What the catalogue did instead was build one image per module
|
||||||
|
around it.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep a process per module.** Rejected: one container per module per node for software that
|
||||||
|
is not a container, and the account-per-module invariant it exists to protect is one the
|
||||||
|
operator declines to pay for.
|
||||||
|
2. **The host executes tools itself.** Rejected: the host is a static Go binary that loads no
|
||||||
|
plugins; a module's tools are TypeScript on the SDK, and building a second SDK in Go for the
|
||||||
|
host's sake is the cost ADR 0039 refuses.
|
||||||
|
3. **One tool runtime per node, a sibling of the host, loading every assigned module's bundle.**
|
||||||
|
Chosen. It is what the runtime was written to be.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. One tool runtime per node, supervised by the host, on the host side — never a container.**
|
||||||
|
The host starts it the way the launcher starts the host
|
||||||
|
([ADR 0005](0005-the-node-host.md)): a process on the machine, restarted when it dies. It holds one
|
||||||
|
bus credential, the node's. It is module-agnostic: it knows bundles and subjects, nothing of what
|
||||||
|
any module does.
|
||||||
|
|
||||||
|
**2. It serves every assigned module's tools and every held seat's verbs** on the subjects the
|
||||||
|
memberships issue. ADR 0159 and ADR 0160 are unchanged in what they say about subjects, grants
|
||||||
|
and memberships; what changes is that one process on the node subscribes to all of them instead of
|
||||||
|
one process per module. A module that runs a long-lived service of its own — a daemon, a
|
||||||
|
container — keeps it; this record is about tools.
|
||||||
|
|
||||||
|
**3. A module brings its tools as a bundle**, the artifact kind the catalogue already has for
|
||||||
|
interpreted code, built by the pipeline and delivered to the node by the host as it delivers any
|
||||||
|
artifact. Never an image. The runtime loads each bundle as the membership names it, and a push
|
||||||
|
that adds or replaces a bundle reaches a running runtime as a reload.
|
||||||
|
|
||||||
|
**4. Root is the module's concern.** A tool that must change the packet filter or rebuild boot
|
||||||
|
images escalates itself. The runtime does not run as root for everyone's sake; the caller does not
|
||||||
|
know and need not.
|
||||||
|
|
||||||
|
**5. Any node may call any tool on any node.** The runtime's credential may call everything, as
|
||||||
|
the console's already may. A per-module calling grant is not kept.
|
||||||
|
|
||||||
|
**6. The console is this runtime's serving mode, renamed.** [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)
|
||||||
|
stands in substance — a module assigned per node, MCP on the machine's loopback, the machine's
|
||||||
|
login is the authority — and changes in form: host-side, serving as well as calling, and named for
|
||||||
|
what it is: **node tools**. The mesh's own verbs stay with the controller
|
||||||
|
([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)); a mesh-scoped seat's verbs
|
||||||
|
run on the node that holds it ([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)).
|
||||||
|
|
||||||
|
**Where ADR 0047 and ADR 0150 say a module's tools are served by the module's own process under
|
||||||
|
the module's own account, read this record.** Everything else they decided stands: a tool is served
|
||||||
|
on its own subject, only the module that serves it answers, a module's long-lived processes are the
|
||||||
|
machine's to supervise. The invariant 0150 kept — one account per module — no longer holds for
|
||||||
|
tools, and the reason is stated above: every tool is callable from everywhere by decision 5, so the
|
||||||
|
account no longer scopes anything a caller cannot already reach.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The packet-filter module's container goes; its verbs run on the host side and escalate as they
|
||||||
|
need. [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md) §3's container capability is moot
|
||||||
|
for it.
|
||||||
|
- The tool runtime's base image stays the way a module's *service* may be built; it is no longer
|
||||||
|
the way tools reach a node.
|
||||||
|
- The node tools runtime needs an interpreter on the machine. The module that is the runtime
|
||||||
|
declares it as a package.
|
||||||
|
- The container-runtime seat proposed in an open change says its holder *runs as a supervised
|
||||||
|
process and serves the verbs locally to the host and on the bus*. A supervised process serving
|
||||||
|
verbs is what this runtime is; whether that holder keeps a process of its own or serves through
|
||||||
|
the runtime is for that record's build to say.
|
||||||
|
- What got harder: one process carries every module's tool code on a node, so one module's
|
||||||
|
faulty bundle can take down the node's tools. The runtime loads each bundle guarded and names
|
||||||
|
the one that failed; the others serve.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| The runtime loads every bundle its memberships name and serves each tool on its subject | the runtime's tests against a real bus: two bundles, three tools, each answers |
|
||||||
|
| A bundle that fails to load is named and the others serve | the same tests, with one bundle that throws on load |
|
||||||
|
| The host supervises the runtime and restarts it | the host's tests over the launcher's shape |
|
||||||
|
| A push that replaces a bundle reloads it without a restart | the runtime's tests: a bundle replaced on disk, the membership re-read, the new tool answers |
|
||||||
|
| No module in the catalogue declares a container whose only purpose is tools | a catalogue check: a manifest with `tools` and an image artifact built on the tool runtime's base is refused once the runtime is live |
|
||||||
|
| Live | `login-shell.execute@<node>` answers on every node from the node tools runtime; `docker ps` shows no per-module tool container |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md)
|
||||||
|
- [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md), [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md),
|
||||||
|
[ADR 0152](0152-the-operators-surface-is-a-module-the-console.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md),
|
||||||
|
[ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md), [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
|
||||||
|
- [To-be 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md), [to-be 34](../03-DESIGN/01-to-be/34-the-console.md)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 176. The login shell is a node seat held by one shell module, and `execute` is its contract
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0040](0040-what-a-module-is.md) names the shell as its example of a *shared* seat: bash, zsh
|
||||||
|
and fish all join `shell`, and one may be default. The operator's reading is sharper, and it
|
||||||
|
matches [ADR 0126](0126-a-module-declares-its-own-seats.md) better: *installing* a shell is
|
||||||
|
installing software, and several may be installed; *holding* the seat is being the login shell,
|
||||||
|
which a node has exactly one of. A definition says which seats a module can hold; the assignment
|
||||||
|
says which it does.
|
||||||
|
|
||||||
|
A seat carries the tools its holder must serve ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)),
|
||||||
|
and [to-be 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) leaves which verbs each
|
||||||
|
seat serves as a decision per seat, taken slowly. This is the first seat of the operator's
|
||||||
|
environment, and the one every node has.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **A shared `shell` seat with a default**, as 0040's example reads. Rejected: *default* is a
|
||||||
|
second concept beside *holder* for the same fact, and the `user` shape already makes the
|
||||||
|
login shell declared state ([to-be 05](../03-DESIGN/01-to-be/05-the-node-host.md)).
|
||||||
|
2. **No seat; each shell module sets the login shell for itself.** Rejected: two assigned shell
|
||||||
|
modules would fight over `chsh`, and nothing would say which won.
|
||||||
|
3. **An exclusive node-scoped seat, `login-shell`, declared by the shell modules, held by one
|
||||||
|
per node.** Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. `login-shell` is a node-scoped seat declared by the shell modules.** zsh, fish and bash each
|
||||||
|
declare that they can hold it; a node's assignment says which does; the controller refuses a
|
||||||
|
second holder by name as for every seat. A shell module that is assigned without holding the seat
|
||||||
|
is installed and nothing more.
|
||||||
|
|
||||||
|
**2. Holding the seat sets the account's login shell.** The holder's declaration carries the
|
||||||
|
`user` shape with the shell it provides, so the login shell is declared state the host applies and
|
||||||
|
gives back when the holding moves — `chsh` stops being a hook.
|
||||||
|
|
||||||
|
**3. The seat's contract is `execute`.** One verb, one argument, the command, run on the node the
|
||||||
|
seat is scoped to as the operator account, answering with what it printed and how it exited.
|
||||||
|
Every holder serves it; a holder may serve its own tools beside it
|
||||||
|
([ADR 0170](0170-the-firewall-seat-serves-its-verbs.md) §2) — show the rendered configuration, list
|
||||||
|
the plugins, set a prompt value.
|
||||||
|
|
||||||
|
**4. Any node may call it on any node.** The grant is the node tools runtime's
|
||||||
|
([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) §5):
|
||||||
|
*run `uptime` on every node* is five calls to one verb.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The first environment module is a shell: a package, files under the home owned by the account,
|
||||||
|
a seat declaration and claim, a `user` shape, and one tool. It proves the whole pattern on every
|
||||||
|
node, servers included, before anything graphical is written.
|
||||||
|
- ADR 0040's shell example is read as *installed is not holding*; a dated note in that record says
|
||||||
|
so. Its decision is untouched.
|
||||||
|
- `execute` is a shell on every machine, addressed over the bus. That is the point, and it is
|
||||||
|
the widest verb the mesh serves; it exists because the operator decided every node may call
|
||||||
|
every tool, and this record does not narrow that.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| Two shell modules assigned to one node, one holding: one `user` shape in the declaration, naming the holder's shell | the controller's composition tests |
|
||||||
|
| A second claimant is refused by name | the catalogue's seat tests |
|
||||||
|
| `execute` runs as the account and answers output and exit status | the module's tool tests over a fake runner, and live on every node |
|
||||||
|
| The seat's verb appears with its scope and machine in the node tools listing | the runtime's tests (to-be 33 §4) |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md)
|
||||||
|
- [ADR 0040](0040-what-a-module-is.md), [ADR 0126](0126-a-module-declares-its-own-seats.md),
|
||||||
|
[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 177. A unit may be user-scoped, and the service manager is a node seat whose holder answers for the units
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The host's `service` shape puts a system unit into a state. It has no user scope.
|
||||||
|
[To-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) states the gap: *a
|
||||||
|
workstation's per-user daemons have no form the mesh can send.* Four of the predecessor's
|
||||||
|
environment modules ship user units — the desktop's reload watcher and bar watchdog, the audio
|
||||||
|
module's masks, the power module's memory guard, the thermal daemon's profile switcher — and the
|
||||||
|
predecessor needed a hook to enable them because *shipping a unit file does not run it*; one unit
|
||||||
|
was deployed for months and ran on one machine only.
|
||||||
|
|
||||||
|
[ADR 0040](0040-what-a-module-is.md) says the host hardcodes no supervisor, and a swappable
|
||||||
|
machine mechanism is a module implementing a capability — which is what the nftables module is for
|
||||||
|
the packet filter ([ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)). The service manager is
|
||||||
|
reported today as a capability, `service-manager`, and held by nobody. The operator's proposal: a
|
||||||
|
systemd module that holds the seat and serves the tools about units, system and user.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep user units as a module concern** — each module runs `systemctl --user` in a hook.
|
||||||
|
Rejected: that is the hook that silently never ran, and an action over the link is refused.
|
||||||
|
2. **The service-manager module applies units** on behalf of others, as a provision. Rejected by
|
||||||
|
the operator: provisioning is for resources a provider creates for a consumer; a unit is
|
||||||
|
declared state the host applies, as every resource is.
|
||||||
|
3. **The host's `service` shape gains a user scope; a systemd module holds the service-manager
|
||||||
|
seat and serves the verbs about units.** Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. The `service` shape gains `scope`: `system` (the default) or `user`.** A user-scoped unit
|
||||||
|
is applied as the operator account through the account's own service manager: enabled, started,
|
||||||
|
stopped, reloaded on its triggers, exactly as a system unit is, and refused on a node with no
|
||||||
|
account, naming the fact. The host applies it; no module does.
|
||||||
|
|
||||||
|
**2. `node-service-manager` is a seat of the mesh's own, node-scoped**, seeded by the controller
|
||||||
|
under this record, as ADR 0121 requires of a `node-*` name. The `systemd` module claims it and is
|
||||||
|
assigned to every machine whose profile reports `service-manager`.
|
||||||
|
|
||||||
|
**3. The seat's verbs answer for every unit on the machine**, each taking an optional `scope`:
|
||||||
|
`units`, `status`, `start`, `stop`, `restart`, `enable`, `disable`, `journal`. The host applies what
|
||||||
|
is declared; the holder answers questions and operator acts about it, and says, for a mesh-held
|
||||||
|
unit, that the host will restore what its declaration says.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The host's vocabulary grows by one field on one shape, asserted by its count test
|
||||||
|
([to-be 05](../03-DESIGN/01-to-be/05-the-node-host.md)); an older host refuses a declaration
|
||||||
|
carrying it, so the host rolls before the first module that uses it.
|
||||||
|
- The predecessor's four user-unit modules become declarable without a hook.
|
||||||
|
- The seat's holder is the first system seat held by a module that runs nothing of its own: its
|
||||||
|
verbs are served by the node tools runtime ([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
|
||||||
|
- What got harder: `journal` and `status` on a user unit need the account's manager reachable
|
||||||
|
from the runtime's process, which runs as the node's account; the holder's tool escalates or
|
||||||
|
switches user as it needs, which is ADR 0175 §4 applied.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A `service` with `scope: user` is enabled and started under the account, and refused with no account | the host's tests with a fake service manager |
|
||||||
|
| The seat declares its verbs; a claim serving fewer is refused by name | the catalogue's seat tests |
|
||||||
|
| The verbs act on a named unit in the named scope and name the unit's holder when the mesh declares it | the module's tests over a fake runner |
|
||||||
|
| Live | the desktop's reload watcher declared `scope: user` on a workstation; `node-service-manager.status@<node>` reports it active |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md)
|
||||||
|
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md),
|
||||||
|
[ADR 0040](0040-what-a-module-is.md), [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)
|
||||||
|
- [To-be 05](../03-DESIGN/01-to-be/05-the-node-host.md), [to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-02
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 180. The found front end is uninstalled once a machine is converged
|
||||||
|
|
||||||
|
> **Renumbered 2026-10-02.** Written and merged as 0175 while another record already held that number on main (one tool runtime per node, merged minutes earlier); `cycle.py` refused main. The branch that lands last renumbers: 0178 and 0179 are claimed by open changes, so this is 0180. Nothing cited it by number.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) retires the firewall a machine
|
||||||
|
was found with by disabling it, never flushing it, and keeps its configuration on disk so that
|
||||||
|
returning the node to adopted can enable it again. [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)
|
||||||
|
made the host keep it retired and say so. Both machines of this mesh that had a front end have been
|
||||||
|
converged for days; neither is going back. What remained of the front end on each — its package,
|
||||||
|
its unit enabled for boot on one, its empty chains still wired into the kernel's hooks, a chain of
|
||||||
|
its container integration still dropping traffic on the IPv6 path until the day before this record
|
||||||
|
— was not a rollback path. It was software nobody runs, left where a reader finds it and asks
|
||||||
|
whether the machine has two firewalls.
|
||||||
|
|
||||||
|
The operator asked on 2026-10-02 that it be disabled and uninstalled. Disabled it already was. For
|
||||||
|
uninstalled, the host had no word: a package could be declared present and not absent.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. A package may be declared absent.** `absent: true` on a package resource has the host remove
|
||||||
|
the package when it is installed and leave alone a machine that never had it, through the machine's
|
||||||
|
own package manager, dependencies untouched. A declaration that stops saying a package is absent
|
||||||
|
installs nothing: there is nothing to undo.
|
||||||
|
|
||||||
|
**2. The module that holds the packet filter seat declares the front end it replaced absent**, after
|
||||||
|
its own filter is loaded, so the mesh's table is in force before the front end's package goes. On a
|
||||||
|
converged machine the front end is therefore gone, not merely off; on an adopted machine nothing of
|
||||||
|
this runs, because the filter module is assigned by the flip and not before.
|
||||||
|
|
||||||
|
**3. Returning such a machine to adopted enables nothing.** A machine with no firewall needs no
|
||||||
|
openings ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)); the host records the
|
||||||
|
front end as *removed*, says so once, and asks nothing of a command that is not there. What
|
||||||
|
ADR 0100 kept on disk for a return is kept only as far as the package manager keeps a changed
|
||||||
|
configuration file; the rollback path it described is given up on purpose.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The host's vocabulary grows by `absent` on a package; an older host refuses a declaration
|
||||||
|
carrying it, so the host rolls before the module.
|
||||||
|
- The nftables module's declaration gains one resource; on the two machines of this mesh that were
|
||||||
|
found with ufw, the next push removes it.
|
||||||
|
- `node show` reads *found firewall: ufw, removed* on those machines from then on.
|
||||||
|
- ADR 0100's sentence about a return to adopted restoring the found firewall holds only while the
|
||||||
|
front end is installed, which after this record it is not on a converged machine.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| An absent package is removed when present, left when not, and read back | host tests over a fake package manager |
|
||||||
|
| An uninstalled front end is recorded as removed and nothing is asked of it | a host test with ufw missing on a converged apply |
|
||||||
|
| Live | the two machines report ufw gone: `pacman -Q ufw` has no answer, `node show` says removed, `status` is well |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)
|
||||||
|
- [Design 08 — Connectivity](../03-DESIGN/01-to-be/08-connectivity.md)
|
||||||
+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
|
||||||
+12
-3
@@ -179,6 +179,10 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **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)
|
- **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)
|
- **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)
|
- **0168** — [A converged machine is filtered by the mesh alone, and the host says what else refuses](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)
|
||||||
|
- **0169** — [A machine joins through the tunnel, and the bus is never public](0169-a-machine-joins-through-the-tunnel-and-the-bus-is-never-public.md)
|
||||||
|
- **0170** — [The firewall seat serves its verbs, and a foreign rule set is removed through one of them](0170-the-firewall-seat-serves-its-verbs.md)
|
||||||
|
- **0172** — [The lab is a module, and runs a bed when the mesh asks](0172-the-lab-is-a-module-and-runs-a-bed-when-the-mesh-asks.md)
|
||||||
|
- **0180** — [The found front end is uninstalled once a machine is converged](0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md)
|
||||||
|
|
||||||
### Its tiers, from the bottom up
|
### Its tiers, from the bottom up
|
||||||
|
|
||||||
@@ -269,9 +273,13 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0150** — [A module's own code runs as supervised processes under the module's one account](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)
|
- **0150** — [A module's own code runs as supervised processes under the module's one account](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)
|
||||||
- **0152** — [The operator's surface is a module the mesh assigns: the console](0152-the-operators-surface-is-a-module-the-console.md)
|
- **0152** — [The operator's surface is a module the mesh assigns: the console](0152-the-operators-surface-is-a-module-the-console.md)
|
||||||
- **0155** — [A definition names no installation: how that is checked, and the three ways a value that did gets out](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
|
- **0155** — [A definition names no installation: how that is checked, and the three ways a value that did gets out](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
|
||||||
- **0164** — [A setting is declared with its default, its meaning and what changing it costs](0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md) *(proposed)*
|
- **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)
|
||||||
- **0165** — [`container-runtime` is what a machine can run; that a runtime is running is its holder's health](0165-container-runtime-is-what-a-machine-can-run-and-a-running-runtime-is-its-holders-health.md) *(proposed)*
|
- **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)
|
||||||
- **0166** — [The container runtime is a node seat, and the host creates containers through its holder](0166-the-container-runtime-is-a-node-seat-and-the-host-creates-containers-through-its-holder.md) *(proposed)*
|
- **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)
|
||||||
|
|
||||||
### How it is built
|
### How it is built
|
||||||
|
|
||||||
@@ -293,6 +301,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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ code:
|
|||||||
- mesh-host internal/apply (the service that reflects a rule set)
|
- mesh-host internal/apply (the service that reflects a rule set)
|
||||||
updated: 2026-10-02
|
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/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/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
|
||||||
@@ -173,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
|
||||||
@@ -743,6 +768,16 @@ tests over a fixture report check the recording, the preview's fates, the status
|
|||||||
predicate. Live: the home server's record names the predecessor's chain as *other* and `status`
|
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.
|
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.**
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ code:
|
|||||||
- mesh-controller internal/inventory
|
- mesh-controller internal/inventory
|
||||||
- mesh-controller internal/catalogue
|
- mesh-controller internal/catalogue
|
||||||
- mesh-controller cmd/mesh-controller
|
- mesh-controller cmd/mesh-controller
|
||||||
updated: 2026-10-01
|
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
|
||||||
@@ -171,6 +173,15 @@ fact, the home as a placement root, and what the mesh may and may not do under a
|
|||||||
decision this document names but no record states. They are the next records to write, before the
|
decision this document names but no record states. They are the next records to write, before the
|
||||||
family of §2 modules is built.
|
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
|
||||||
@@ -205,6 +216,10 @@ keeping the predecessor running, so the model questions above are no longer defe
|
|||||||
record, the home as a placement root, user-scoped services and the one-off steps a hook used to run
|
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.
|
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
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: implemented
|
status: implemented
|
||||||
code: [mesh-controller, mesh-tools]
|
code: [mesh-controller, mesh-tools]
|
||||||
updated: 2026-10-01
|
updated: 2026-10-02
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md
|
||||||
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
- 02-DECISIONS/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/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
|
||||||
- 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
|
- 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
|
||||||
@@ -121,6 +122,8 @@ module-specific names that changes the day the forge is replaced.
|
|||||||
asked of the module, through a `tools` verb every runtime answers) and lists a role's tools when the
|
asked of the module, through a `tools` verb every runtime answers) and lists a role's tools when the
|
||||||
records carry them.
|
records carry them.
|
||||||
|
|
||||||
|
*Amended 2026-10-02 by [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md):* the module that serves this to an agent is the node tools runtime — one per node, host-side, serving every assigned module's tools as well as answering the person on loopback. The console is its serving mode, renamed. See [37 — The operator's machine](37-the-operators-machine.md) §3.
|
||||||
|
|
||||||
## 7. Versioning
|
## 7. Versioning
|
||||||
|
|
||||||
A seat's tools are an interface and change like one. Additive within a version. A change that would
|
A seat's tools are an interface and change like one. Additive within a version. A change that would
|
||||||
@@ -169,6 +172,18 @@ either way.
|
|||||||
What stays as designed and not built: which verbs any *other* seat serves, and §3 for module-declared
|
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.
|
seats' schemas beyond the names their manifests already list.
|
||||||
|
|
||||||
|
## The firewall seat's verbs, 2026-10-02
|
||||||
|
|
||||||
|
[ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md). The first node-scoped seat
|
||||||
|
to carry verbs: `node-packet-filter` serves `rules` (the filter as the machine enforces it, nftables
|
||||||
|
and legacy), `reload` (the mesh's own filter from its file) and `remove` (one rule set the mesh did
|
||||||
|
not write, named as the host reports it under ADR 0168; refusing the mesh's tables, the runtime's
|
||||||
|
own chains, a built-in chain and an active found firewall's). Every holder serves all three; the
|
||||||
|
nftables module does so from a runtime on the machine's network with `NET_ADMIN`, which is the first
|
||||||
|
container to declare a capability. Removing a predecessor's rule set is an operator's act reached
|
||||||
|
through the seat, recorded on the bus, instead of a shell on the machine. *How it is checked:* ADR
|
||||||
|
0169's table.
|
||||||
|
|
||||||
## What this does not settle
|
## What this does not settle
|
||||||
|
|
||||||
- Which verbs each seat should serve. That is a decision per seat, and the reason to do it slowly: a
|
- Which verbs each seat should serve. That is a decision per seat, and the reason to do it slowly: a
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: implemented
|
status: implemented
|
||||||
code: [mesh-catalog, mesh-tools, mesh-controller]
|
code: [mesh-catalog, mesh-tools, mesh-controller]
|
||||||
updated: 2026-10-01
|
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/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/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
|
||||||
@@ -20,6 +20,8 @@ An agent reaches them over MCP on the machine's loopback; a person reaches the s
|
|||||||
installed by hand, nothing is configured with an address, and the mesh knows the surface exists because
|
installed by hand, nothing is configured with an address, and the mesh knows the surface exists because
|
||||||
it put it there ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
|
it put it there ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
|
||||||
|
|
||||||
|
> **Amended 2026-10-02 by [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** What this document describes stays true in substance and changes in form: the console becomes the serving mode of the node tools runtime, a host-side process the host supervises rather than a container, which also serves every assigned module's tools from their bundles. The module is renamed `node-tools`. [37 — The operator's machine](37-the-operators-machine.md) §3 is where it now lives.
|
||||||
|
|
||||||
## 1. What it is
|
## 1. What it is
|
||||||
|
|
||||||
A module, `mesh-console`, in the catalogue. Its image is the tool runtime's own — the client that
|
A module, `mesh-console`, in the catalogue. Its image is the tool runtime's own — the client that
|
||||||
@@ -28,6 +30,14 @@ module's credential and listens on loopback. It has no state, no provision, no s
|
|||||||
the bus, which it gets the way every module does: a credential the mesh minted for `<node>.mesh-console`,
|
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.
|
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:
|
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;
|
- `invokes: ["*"]` — it calls every tool on the mesh, and the bus grants exactly that publish side;
|
||||||
|
|||||||
@@ -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,193 @@
|
|||||||
|
---
|
||||||
|
layer: to-be
|
||||||
|
status: in-progress
|
||||||
|
code: [mesh-tools, mesh-controller, mesh-host, mesh-catalog]
|
||||||
|
updated: 2026-10-02
|
||||||
|
decisions:
|
||||||
|
- 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
|
||||||
|
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
|
||||||
|
- 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md
|
||||||
|
- 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md
|
||||||
|
- 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
|
||||||
|
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||||
|
- 02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 38. Building the operator's machine
|
||||||
|
|
||||||
|
**The work of [design 37](37-the-operators-machine.md), broken into packages small enough that each
|
||||||
|
ends at something a person can see run, in the order their dependencies allow.** Design 37 is the
|
||||||
|
authority on *what* is built; this document holds only the packages, their order, their sizes and
|
||||||
|
their proofs, and is wrong the moment it disagrees with 37 rather than the other way round. It is
|
||||||
|
the shape [design 28](28-building-the-bus.md) gave the bus work, applied here.
|
||||||
|
|
||||||
|
## How this is built, and where it is run
|
||||||
|
|
||||||
|
**On the live mesh, by the operator's decision.** Every package is written with unit tests and
|
||||||
|
committed on one branch per repository; its proof runs on the four machines, not in the lab.
|
||||||
|
[ADR 0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md) already says the live mesh is
|
||||||
|
the test bed; the operator's words on 2026-10-02 were *skip the lab, it is not too bad if something
|
||||||
|
is broken*. The cost accepted: a package that breaks the runtime breaks every tool on a node until
|
||||||
|
the next push, and the controller's own verbs stay reachable through the controller seat whatever
|
||||||
|
happens to a node's runtime — which is the one thing that must hold, and does by construction
|
||||||
|
([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)).
|
||||||
|
|
||||||
|
Each package names what proves it. A package that cannot name its proof is divided until it can.
|
||||||
|
|
||||||
|
## What exists already, measured
|
||||||
|
|
||||||
|
Counted 2026-10-02 in the four repositories, non-test source. The point of the count is the same
|
||||||
|
as design 28's: nothing here is new ground; every package reshapes something standing.
|
||||||
|
|
||||||
|
| Piece | Today | Size | Becomes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| the tool runtime | TypeScript: loads `MESH_TOOL_MODULES`, serves one module's tools and its claimed seats' verbs; `serve` is the console | ~1 700 lines over six files | loads every assigned module's bundle; `serve` is node tools |
|
||||||
|
| the host's `process` shape | Go: fetch a bundle by digest, unpack under the mesh's daemons directory, write the unit, run it | 343 lines | **unchanged** — the runtime is one such process |
|
||||||
|
| the host's `archive` shape | Go: fetch and unpack an artifact at a path | 185 lines | **unchanged** — a module's tools bundle is one such archive |
|
||||||
|
| the controller's bus principals | Go: one principal per module per node, grants from what it declares | 132 lines | gains one principal per node for the runtime |
|
||||||
|
| the controller's memberships | Go: one per assignment, the subjects a runtime serves | 143 lines | **unchanged** in shape; the runtime reads several |
|
||||||
|
| the controller's declaration composer | Go, one file | 2 053 lines | gains the runtime's process, the bundles' archives, two env words |
|
||||||
|
| the catalogue | 35 manifests build a per-module tool container on the runtime's base image | — | none do; the runtime is a module of its own |
|
||||||
|
|
||||||
|
**Two measurements decide the shape.** The host needs no change: a `process` and an `archive` are
|
||||||
|
what the runtime and a bundle are, and both are applied today. And the runtime already does
|
||||||
|
nine-tenths of the job — the loop over entrypoints, the seat verbs, the membership subscription —
|
||||||
|
for one module; the work is to let it do the same for a list.
|
||||||
|
|
||||||
|
## The order the work allows
|
||||||
|
|
||||||
|
```
|
||||||
|
WP1 the runtime serves many modules (mesh-tools) ──┐
|
||||||
|
WP2 the controller composes one runtime a node (mesh-controller) ──┤ independent, test-proven
|
||||||
|
│
|
||||||
|
WP3 the runtime is a module; the console is its serving mode (mesh-tools, mesh-catalog)
|
||||||
|
│
|
||||||
|
WP4 the first holder moves: the packet filter (mesh-catalog) ── the live proof
|
||||||
|
│
|
||||||
|
WP5 the shell, on a server (mesh-catalog) ── the first environment module live
|
||||||
|
WP6 the service manager, on a workstation (mesh-host #72, mesh-catalog)
|
||||||
|
│
|
||||||
|
WP7 the login manager, the display server, the window manager … ── one record per seat, after this document
|
||||||
|
WP8 settings for the theme knobs ── after issue 168 closes
|
||||||
|
```
|
||||||
|
|
||||||
|
WP1 and WP2 touch different repositories and meet only at the membership's shape, which neither
|
||||||
|
changes; they are built in parallel. WP3 needs both. WP4 is the first time anything on a machine
|
||||||
|
changes, and it is the proof of the whole. WP5 and WP6 are the first environment modules; the
|
||||||
|
packages after them are design 37 §4's candidates and are not broken down here, because each
|
||||||
|
begins with a decision record this document cannot anticipate.
|
||||||
|
|
||||||
|
## WP1 — The runtime serves many modules
|
||||||
|
|
||||||
|
*mesh-tools. About a day.*
|
||||||
|
|
||||||
|
**What changes.** `serve` takes a list of modules to serve, each with its entrypoints, rather than
|
||||||
|
one module and one credential. The runtime reads one membership per module from the subjects
|
||||||
|
[ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||||
|
derives for each, and serves each module's tools on that module's subjects and each held seat's
|
||||||
|
verbs on the seat's. The filter that drops a registration under any name but the one module goes;
|
||||||
|
what remains is the rule that a registration under a seat's name is served only where some module
|
||||||
|
the runtime serves claims that seat. A bundle that throws on import is named in the log and in
|
||||||
|
what `tools` answers, and the others serve. The runtime reads `MESH_OPERATOR_ACCOUNT` and
|
||||||
|
`MESH_OPERATOR_HOME` and hands them to every tool's environment.
|
||||||
|
|
||||||
|
**What does not change.** The SDK. The broker client. The MCP surface. A module's tool code.
|
||||||
|
|
||||||
|
**Proof.** The runtime's test against a real bus: three bundles, one of which throws on import;
|
||||||
|
five tools and two seat verbs answer on their subjects; `tools` names the failed bundle; a
|
||||||
|
membership republished mid-run re-subscribes without a restart.
|
||||||
|
|
||||||
|
## WP2 — The controller composes one runtime per node
|
||||||
|
|
||||||
|
*mesh-controller. Two to three days; the largest package.*
|
||||||
|
|
||||||
|
**What changes**, in four pieces, each its own commit:
|
||||||
|
|
||||||
|
1. **A node principal.** Beside one principal per module per node, one per node of kind
|
||||||
|
`node-tools`: its serving grants are the union of every assigned module's tool subjects and every
|
||||||
|
held seat's verbs on that node, its invoking grant is `*`, and it consumes nothing. The
|
||||||
|
per-module memberships are composed as today; nothing else on the bus learns a new shape.
|
||||||
|
2. **Bundle delivery.** For every assigned module whose build produced a `bundle`, the node's
|
||||||
|
declaration gains an `archive` placed under a directory the controller derives, so the host
|
||||||
|
fetches and unpacks it as it does any artifact. The bundle's digest is what the build recorded.
|
||||||
|
3. **The runtime's process.** One `process` per node running the runtime from its own bundle
|
||||||
|
(WP3), `MESH_TOOL_MODULES` composed from the unpacked entrypoints, `MESH_OPERATOR_ACCOUNT` and
|
||||||
|
`MESH_OPERATOR_HOME` from the account fact, `restart-on` naming every bundle so a push that
|
||||||
|
changes one restarts it. A node with no account composes the runtime without the two words.
|
||||||
|
4. **The gate.** A manifest declaring `tools` and a container built on the runtime's base image is
|
||||||
|
refused at registration once the runtime module is registered, naming this record. It is the
|
||||||
|
mechanism that keeps the old pattern from returning by habit.
|
||||||
|
|
||||||
|
**Proof.** Composition tests: a node with three assigned modules, one holding a seat, yields one
|
||||||
|
process, three archives, one node principal whose grants are the union, and the same three
|
||||||
|
memberships as before. The gate's test: the packet-filter manifest as it is today is refused once
|
||||||
|
the runtime is registered.
|
||||||
|
|
||||||
|
## WP3 — The runtime is a module, and the console is its serving mode
|
||||||
|
|
||||||
|
*mesh-tools and mesh-catalog. A day.*
|
||||||
|
|
||||||
|
**What changes.** mesh-tools gains a `bundle` artifact of itself beside its images, and its manifest
|
||||||
|
becomes the `node-tools` module: a package for the interpreter, the loopback listener the console
|
||||||
|
declared, `invokes: *`, and nothing else — the process is the controller's to compose (WP2). In the
|
||||||
|
catalogue, `mesh-console` is retired as a module and `node-tools` assigned where it was. The
|
||||||
|
runtime's `serve` keeps answering MCP on loopback; the person's end of it keeps the name *console*
|
||||||
|
([glossary](../../00-META/glossary.md)).
|
||||||
|
|
||||||
|
**Proof.** On every node: the console's container is gone, `node-tools` runs as a unit the host
|
||||||
|
wrote, `tools/list` on loopback answers as before, and the controller's verbs answer through it.
|
||||||
|
This is the first live step, and it is reversible by re-assigning `mesh-console`.
|
||||||
|
|
||||||
|
## WP4 — The first holder moves: the packet filter
|
||||||
|
|
||||||
|
*mesh-catalog. Half a day. The live proof of ADR 0175.*
|
||||||
|
|
||||||
|
**What changes.** The nftables module drops its container, its `NET_ADMIN` and its runtime
|
||||||
|
artifact; its tools bundle stays and its claim stays. Its `remove` and `reload` escalate inside the
|
||||||
|
tool where they need root, which they have, since the runtime runs as the node's account.
|
||||||
|
|
||||||
|
**Proof.** `node-packet-filter.rules@<node>`, `reload` and `remove` answer from the runtime on all
|
||||||
|
four machines; `docker ps` shows no `mesh-nftables`; `status` is well. Then the fail2ban holder
|
||||||
|
proposed in an open change follows the same way when it lands.
|
||||||
|
|
||||||
|
## WP5 — The shell, on a server first
|
||||||
|
|
||||||
|
*mesh-catalog #224, already written. Half a day to assign and prove.*
|
||||||
|
|
||||||
|
**Order.** Assign `zsh` to one server; push; `login-shell.execute@<server> command="uptime"`
|
||||||
|
answers; the account's login shell reads zsh; its `~/.zshrc` carries the mesh's block with the
|
||||||
|
operator's lines around it. Then the other three nodes. The two things the manifest cannot say
|
||||||
|
— the `user` shape applying only where the seat is held, and a second shell module installed
|
||||||
|
beside the holder — are the first follow-up record after this document.
|
||||||
|
|
||||||
|
## WP6 — The service manager, on a workstation
|
||||||
|
|
||||||
|
*mesh-host #72 merged first; mesh-catalog #224. Half a day.*
|
||||||
|
|
||||||
|
**Order.** Merge the host's user-scope change and let it roll. Assign `systemd` everywhere;
|
||||||
|
`node-service-manager.units@<node> scope=user` answers on a workstation. Then the first user-scoped
|
||||||
|
unit the mesh sends: the window manager's reload watcher, declared `scope: user` by the window
|
||||||
|
manager module when WP7 writes it — until then, the host's change is proven by its tests and by
|
||||||
|
the verb answering.
|
||||||
|
|
||||||
|
## What is deliberately not here
|
||||||
|
|
||||||
|
- **The graphical stack's seats** (WP7). Each begins with a record naming its holders and verbs,
|
||||||
|
and the first graphical module asks the resolver a question this document cannot answer for it:
|
||||||
|
whether a held seat gates another's assignment.
|
||||||
|
- **Settings for the theme knobs** (WP8). Blocked on the settings record proposed in an open change
|
||||||
|
and on [issue 168](../../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md).
|
||||||
|
- **Reload without restart.** WP2 restarts the runtime on a bundle change; a reload that keeps the
|
||||||
|
other modules' tools up during one module's change is a refinement for after WP4 proves the
|
||||||
|
simple form.
|
||||||
|
- **Lingering.** A user-scoped unit answers only while the account's manager runs; declaring
|
||||||
|
lingering for the account is a field on the `user` shape, decided when a server first needs a
|
||||||
|
user unit.
|
||||||
|
|
||||||
|
## How this list is kept true
|
||||||
|
|
||||||
|
Each package's proof is run on the live mesh when the package is finished and its line here gains
|
||||||
|
the date and the commit, the way [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md)
|
||||||
|
carries *built and proven live*. A package whose proof fails is not reworded; the failure is
|
||||||
|
recorded under it and the package stays open. When WP6 is proven, design 37's status moves to
|
||||||
|
`implemented` for what it covers and this document's to the same.
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
---
|
||||||
|
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/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/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
|
||||||
|
- 02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md
|
||||||
|
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 40. Building the operator's agent and its licence manager
|
||||||
|
|
||||||
|
**The work of [design 36](36-the-operators-agent-on-a-machine.md) and [design 39](39-the-anthropic-licence-manager.md),
|
||||||
|
broken into packages that each end at something a person can see run, in the order their
|
||||||
|
dependencies allow.** The two designs are the authority on *what* is built; this document holds the
|
||||||
|
packages, their order, their sizes and their proofs, and is wrong the moment it disagrees with them.
|
||||||
|
It is the shape [design 38](38-building-the-operators-machine.md) gave the operator's machine, applied
|
||||||
|
to the two modules that make its agent work.
|
||||||
|
|
||||||
|
## How this is built, and where it is run
|
||||||
|
|
||||||
|
**On the live mesh, by the operator's decision** ([ADR 0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md),
|
||||||
|
and design 38's words on the same day). Every package is written with unit tests, committed on one
|
||||||
|
branch per repository ([playbook 07](../../00-META/process/07-feature-branches.md)), and proven on the
|
||||||
|
machines: the control node first for the manager, one workstation first for the agent, then the rest.
|
||||||
|
The cost accepted: a broken agent module leaves a workstation's agent without the mesh's instructions
|
||||||
|
or with a stale token until the next push; the person's own files under the home are never in reach of
|
||||||
|
the failure, by [ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.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 repositories and on the machines. Nothing here is new ground; every package
|
||||||
|
reshapes something standing.
|
||||||
|
|
||||||
|
| Piece | Today | Becomes |
|
||||||
|
|---|---|---|
|
||||||
|
| the vendor's refresh, the sealed box, the grant file | `anthropic-manager` in the catalogue: a token-endpoint client, a sealed-box primitive, adoption of a grant sealed to a node's key; assigned to nothing | the manager's refresh and adoption, with the lease, the floor and the cadence the predecessor's manager had |
|
||||||
|
| the credentials write, the refresh-token strip, the identity read | `anthropic-consumer` in the catalogue: tested; assigned to nothing | the agent module's write, unchanged in shape |
|
||||||
|
| the predecessor's manager and consumer | two modules in the retired system: the lease per licence, the expiry floor, the lineage comparison, the identity guard, three touchpoints with fallbacks, cooldowns on alarms | ported as logic with its tests; nothing of its registry or its bus |
|
||||||
|
| the host's `process` and `archive` shapes | fetch a bundle by digest and run it supervised; fetch and unpack an artifact | **unchanged** — the manager's daemon is one process; both modules' tools are bundles |
|
||||||
|
| the manifest's `uses`, `claims`, `invokes seat:<seat>.<verb>`, node-scoped `provides` with `serves: {port}` | all four exist and are used by other modules | **unchanged** — the agent uses the seat and invokes its verbs; the console provides its endpoint |
|
||||||
|
| the console | a container per node, MCP on loopback, no provision | gains one provision; becomes the node-tools runtime's serving mode under design 38's WP3 |
|
||||||
|
| the agent's package | present on both workstations from a build the predecessor's helper made; the distribution's repositories do not carry it | declared; satisfied where present, refused where not, until a package repository seat exists |
|
||||||
|
| the operator account | a column on every node record, **empty on all four** | stated by the operator, before anything home-scoped lands |
|
||||||
|
|
||||||
|
**One dependency decides the order.** Both modules serve tools and the agent module's tools write
|
||||||
|
under `/etc` and, as the operator, under the home. Under [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)
|
||||||
|
tools run in the node's tool runtime, host-side, which design 38 builds in its WP1–WP3. Writing a
|
||||||
|
per-module tool container for these two modules would be building the pattern that record retires, so
|
||||||
|
**the live proofs of WP3 to WP5 below wait for design 38's WP3.** Everything before a live proof —
|
||||||
|
manifests, code, tests — does not, and is written now.
|
||||||
|
|
||||||
|
## The order the work allows
|
||||||
|
|
||||||
|
```
|
||||||
|
WP0 the operator states the facts (the live mesh) ── accounts, roles, licences to adopt
|
||||||
|
WP1 the console provides its endpoint (mesh-catalog) ── small, independent
|
||||||
|
WP2 the licence manager, built and tested (mesh-catalog) ──┐ independent of each other;
|
||||||
|
WP3 the agent module, built and tested (mesh-catalog) ──┘ both wait on design 38 WP3 to run
|
||||||
|
│
|
||||||
|
WP4 the manager live on the control node (the live mesh) ── three licences adopted, a refresh seen
|
||||||
|
WP5 the agent live on one workstation (the live mesh) ── the hand-over, the switch, the instructions
|
||||||
|
WP6 the rest of the nodes, and the predecessor's remains ── adoption from a login, the retirements
|
||||||
|
```
|
||||||
|
|
||||||
|
WP1, WP2 and WP3 touch different directories of one repository and meet only at the seat's name and
|
||||||
|
the provision's name; they are built in parallel. WP4 is the first time anything on a machine changes.
|
||||||
|
WP5 is the proof of the whole.
|
||||||
|
|
||||||
|
## WP0 — The operator states the facts
|
||||||
|
|
||||||
|
*The live mesh. An hour, and it is the operator's.*
|
||||||
|
|
||||||
|
The account on each node record, through the controller's node command — none is stated today, and
|
||||||
|
[ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)
|
||||||
|
refuses a home-scoped module without one. The role of each node, as the agent module's setting on the
|
||||||
|
node layer, once the module exists. Which three licences exist and what each is called.
|
||||||
|
|
||||||
|
**Proof.** The controller's node command lists an account for every node.
|
||||||
|
|
||||||
|
## WP1 — The console provides its endpoint
|
||||||
|
|
||||||
|
*mesh-catalog. Half a day.*
|
||||||
|
|
||||||
|
**What changes.** The console's manifest gains a node-scoped provision — working name
|
||||||
|
`console-endpoint`, fixed when the manifest is written — serving the port the machine gave it, as the
|
||||||
|
local model server already does for its API. Co-location resolves it
|
||||||
|
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md),
|
||||||
|
[to-be 34](34-the-console.md) as amended). When design 38's WP3 moves the console into the node-tools
|
||||||
|
module, the provision moves with it; it is a line in a manifest either way.
|
||||||
|
|
||||||
|
**Proof.** The controller's plan for a workstation shows a consumer of the provision bound to the
|
||||||
|
console's port; the same consumer on a machine without the console is refused naming the provision;
|
||||||
|
the catalogue's tests pass.
|
||||||
|
|
||||||
|
## WP2 — The licence manager, built and tested
|
||||||
|
|
||||||
|
*mesh-catalog. Two to three days; the largest package.*
|
||||||
|
|
||||||
|
**What is written**, as design 39 says:
|
||||||
|
|
||||||
|
1. **The manifest.** Claims the mesh-scoped seat `anthropic-licence-manager` with its verbs; requires a
|
||||||
|
database, a `secret` for the key its grants are encrypted with, and the bus; a `bundle` of tools; a
|
||||||
|
`process` for the daemon that refreshes, collects usage and notifies, on a schedule; declared
|
||||||
|
settings for the cadence, the usage threshold and the cooldown, each with a default; `invokes` the
|
||||||
|
agent module's `apply`.
|
||||||
|
2. **The store.** Migrations for licences, bindings, usage and audit, with the lease and the
|
||||||
|
notification slot as columns, numbered and idempotent.
|
||||||
|
3. **The refresh.** The token-endpoint client and the sealed box from `anthropic-manager`; the plan
|
||||||
|
(floor, cadence, forced, cannot) and the lease from the predecessor, as pure functions with their
|
||||||
|
tests; the vendor's reason logged on failure; counted failures, one notification per cooldown.
|
||||||
|
4. **Adoption.** From a file on the manager's node for the API key; from a sealed grant a node offers;
|
||||||
|
the identity guard that refuses a mismatch and notifies.
|
||||||
|
5. **Usage.** The vendor's reading per licence on a schedule, stored raw and normalised
|
||||||
|
([ADR 0054](../../02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md)), one notification
|
||||||
|
per threshold crossing.
|
||||||
|
6. **The verbs**: `licences`, `bindings`, `bind`, `switch`, `release`, `refresh`, `usage`, `adopt`,
|
||||||
|
`register`, `current` — the last answering a consumer's token sealed to the key that consumer
|
||||||
|
registered.
|
||||||
|
7. **The hand-over**: on rotation or switch, one call to `claude-code.apply@<node>` per bound node,
|
||||||
|
the token sealed to that node's key, the answer recorded.
|
||||||
|
|
||||||
|
**Proof, before anything runs live.** Unit tests: two refresh runs started together rotate one grant
|
||||||
|
once; a grant with a mismatching identity is refused; a worker bound to a dead licence is refused and
|
||||||
|
never lent another; every event the daemon emits is free of a token; the hand-over payload opens only
|
||||||
|
with the registered key. The catalogue's checks: no installation named, no secret in a declared file.
|
||||||
|
|
||||||
|
## WP3 — The agent module, built and tested
|
||||||
|
|
||||||
|
*mesh-catalog. Two days.*
|
||||||
|
|
||||||
|
**What is written**, as design 36 says:
|
||||||
|
|
||||||
|
1. **The manifest.** The agent's package; the state directory; the facts file carrying the node's
|
||||||
|
name, the operator account and its home, the console's bound port, the role and the extra tool
|
||||||
|
servers from settings; requires the console's endpoint and the bus; `uses` the seat and `invokes`
|
||||||
|
its `register`, `current` and `adopt`; a `bundle` of tools. **No file resource under a home or
|
||||||
|
under `/etc`.**
|
||||||
|
2. **The renderer.** From the facts file and the current binding, the managed settings file (the tool
|
||||||
|
servers under the entry `mesh`, the attribution trailers, and the key-helper for an API-key binding)
|
||||||
|
and the managed instruction file (§3 of design 36), written under the agent's managed directory
|
||||||
|
with the escalation the tool performs for itself; idempotent; re-run when the facts file changes.
|
||||||
|
3. **The keypair**, made once in the state directory, the public half registered with the seat at
|
||||||
|
start and at every start.
|
||||||
|
4. **The consumer side**: `apply` (a rotation applied only if newer within one lineage, a switch applied
|
||||||
|
regardless, the answer naming the outcome and never a token); the pull at start and near expiry;
|
||||||
|
the credentials write as the operator, access-token-only, from `anthropic-consumer` with its tests;
|
||||||
|
the key-helper program for the API key; the offer of a login to the seat, sealed, after reading the
|
||||||
|
account's identity.
|
||||||
|
5. **The tools**: `apply`, `licence_status`, `mcp_configure` (validates a server, sets the module's
|
||||||
|
setting through the controller's settings verb), `render` (re-render now, for a person).
|
||||||
|
6. **The documentation**: the six predecessor files and the hand-made console entry a person removes
|
||||||
|
on a workstation that carried the predecessor.
|
||||||
|
|
||||||
|
**Proof, before anything runs live.** Unit tests: the renderer writes only the mesh's keys and leaves
|
||||||
|
every other key of a seeded settings file; the credentials write strips a refresh token and is atomic;
|
||||||
|
the lineage comparison from the predecessor, with its cases; a login offer carries the identity it read.
|
||||||
|
The catalogue's checks pass.
|
||||||
|
|
||||||
|
## WP4 — The manager live on the control node
|
||||||
|
|
||||||
|
*The live mesh. Half a day, after design 38's WP3.*
|
||||||
|
|
||||||
|
**Order.** Assign the manager on the control node; push. Adopt the API key from a file there. Adopt
|
||||||
|
the two subscription grants: a login in a throwaway home on the control node, offered to the seat the
|
||||||
|
way a node's agent module will. Bind each node's agent to a licence.
|
||||||
|
|
||||||
|
**Proof.** Through the console: `anthropic-licence-manager.licences` lists three licences with
|
||||||
|
identity and expiry; within the cadence, the audit shows a rotation and `licences` shows a later
|
||||||
|
expiry; a forced `refresh` on one licence is logged with the vendor's answer; the two retired catalogue
|
||||||
|
modules are still assigned to nothing.
|
||||||
|
|
||||||
|
## WP5 — The agent live on one workstation
|
||||||
|
|
||||||
|
*The live mesh. Half a day. The proof of the whole.*
|
||||||
|
|
||||||
|
**Order.** Set the workstation's role in the module's settings. Record the checksums of everything
|
||||||
|
under the person's agent directory. Assign the module; push. Remove the six predecessor files and the
|
||||||
|
hand-made console entry. Start a new session.
|
||||||
|
|
||||||
|
**Proof.** The managed directory holds the settings and instruction files, owned by root. Everything
|
||||||
|
under the person's agent directory is byte-identical to before except the credentials file, which is
|
||||||
|
owned by the operator, readable by nobody else, and names no refresh token. The new session lists the
|
||||||
|
mesh's tools under `mesh` once, answers *which node am I* from the instruction file, and makes a model
|
||||||
|
request. `anthropic-licence-manager.switch` to the second subscription licence changes the token on
|
||||||
|
the workstation within a minute, and neither the verb's answer nor either module's log holds a token.
|
||||||
|
Switched to the API-key licence, the credentials file is left as it was and the agent authenticates
|
||||||
|
through the key-helper. Switched back.
|
||||||
|
|
||||||
|
## WP6 — The rest of the nodes, and the predecessor's remains
|
||||||
|
|
||||||
|
*The live mesh and mesh-catalog. One day.*
|
||||||
|
|
||||||
|
**Order.** Assign the module on the second workstation and on the servers whose account is stated;
|
||||||
|
remove the predecessor's files on the second workstation. Log in on a workstation under a licence's
|
||||||
|
account and watch the offer be adopted — and under the wrong account, and watch it refused and
|
||||||
|
notified. Retire `anthropic-manager` and `anthropic-consumer` from the catalogue. Set designs 36 and
|
||||||
|
39 to `implemented` for what runs, with the as-is written
|
||||||
|
([playbook 02](../../00-META/process/02-graduation.md)).
|
||||||
|
|
||||||
|
**Proof.** Every node with an account runs the module and `licence_status` answers on each; the
|
||||||
|
refused login's notification arrived; the catalogue has no module built on the old placement.
|
||||||
|
|
||||||
|
## What is deliberately not here
|
||||||
|
|
||||||
|
- **The package repository seat** for a distribution that does not carry the agent's package
|
||||||
|
(design 36 §7). A fresh node refuses the module in the package manager's words until it exists.
|
||||||
|
- **An automated switch on exhaustion.** The readings are kept from WP4; the policy is a later record.
|
||||||
|
- **Workers and the mesh's own sessions as consumers.** The manager's bindings and fallbacks know them
|
||||||
|
from WP2; the consumers themselves do not exist yet ([to-be 15](15-the-agent-session.md),
|
||||||
|
[ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md)).
|
||||||
|
- **Whether a refresh token is single-use.** WP4 may measure it on a licence deliberately refreshed
|
||||||
|
twice; the design holds either way.
|
||||||
|
|
||||||
|
## How this list is kept true
|
||||||
|
|
||||||
|
Each package's proof is run when the package is finished and its line here gains the date and the
|
||||||
|
commit, as design 38 does. A package whose proof fails is not reworded; the failure is recorded under
|
||||||
|
it and the package stays open. When WP5 is proven, designs 36 and 39 move to `in-progress` with their
|
||||||
|
owning repository, and when WP6 is proven to `implemented`, with the as-is written.
|
||||||
@@ -41,6 +41,8 @@ document is written and this one's status becomes `implemented`.
|
|||||||
| [`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) |
|
| [`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
|
||||||
|
|
||||||
|
|||||||
-85
@@ -1,85 +0,0 @@
|
|||||||
---
|
|
||||||
status: located
|
|
||||||
opened: 2026-10-01
|
|
||||||
located-in: [mesh-catalog modules/dnsmasq, mesh-controller internal/overlay/generator.go, mesh-controller internal/catalogue/resolve.go (checkResources)]
|
|
||||||
fixed-by:
|
|
||||||
amended-design:
|
|
||||||
---
|
|
||||||
|
|
||||||
# 190 — The container runtime's configuration is written by modules that are not the runtime's
|
|
||||||
|
|
||||||
## What was observed
|
|
||||||
|
|
||||||
The runtime's configuration file and its service are declared by two parties, neither of which is
|
|
||||||
the runtime:
|
|
||||||
|
|
||||||
- **The resolver module** writes the runtime's `dns` key (the machine's private address) and
|
|
||||||
`live-restore` into the runtime's file, written into rather than over
|
|
||||||
([ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md)). It also
|
|
||||||
declares the runtime's service, reloaded when that file changes. The `dns` key has been written
|
|
||||||
since the resolver module was converted from its predecessor on 2026-09-23; `live-restore` and the
|
|
||||||
service were added on 2026-09-30 while fixing
|
|
||||||
[issue 110](../110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md),
|
|
||||||
where containers silently resolved through a public resolver.
|
|
||||||
- **The private network** writes the runtime's `insecure-registries` into the same file, and declares
|
|
||||||
the same service reloaded on it, as [ADR 0082](../../02-DECISIONS/0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md)
|
|
||||||
and ADR 0102 decided. The controller generates both resources per machine.
|
|
||||||
|
|
||||||
On the three machines that run the resolver module, both declare one path and one unit. Nothing refuses
|
|
||||||
it. The collision check compares the resources of catalogue modules. The private network is computed,
|
|
||||||
so its resources are produced when a machine's declaration is composed, and the check never sees them.
|
|
||||||
|
|
||||||
The machine without the resolver module shows the other half. Its runtime still has the predecessor's
|
|
||||||
resolver and `live-restore` off, because the only module that sets them is a DNS server. A machine
|
|
||||||
gets a correct container runtime only as a side effect of being given a resolver.
|
|
||||||
|
|
||||||
> **Later the same day, 2026-10-02.** The resolver module and its sibling for the resolver file were
|
|
||||||
> assigned to the fourth machine ([issue 198](../198-the-lans-dns-server-ran-outside-the-mesh-and-its-filter-closed-it/00-report.md)),
|
|
||||||
> so all four now have the resolver writing into the runtime's file, and the predecessor's
|
|
||||||
> `live-restore: false` there is gone. The same work made the runtime's file, as the resolver declares
|
|
||||||
> it, take no settings: a setting meant for the resolver's own configuration had reached it. The
|
|
||||||
> collision and the ownership question above are unchanged.
|
|
||||||
|
|
||||||
## Why this is here
|
|
||||||
|
|
||||||
The operator ruled it a defect, not a design: **a module does not write another software's
|
|
||||||
configuration.** The need behind each write is real. Containers must resolve the mesh's names
|
|
||||||
([ADR 0148](../../02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md) step 2).
|
|
||||||
A daemon restart must not stop every container. Every machine on the network must trust the mesh's
|
|
||||||
registry. But each of these is a fact the runtime must be *given*, and the module that gives it is the
|
|
||||||
runtime's own. With three writers, nobody can say what the file should contain. Two of the facts are
|
|
||||||
reloaded when one of them needs a restart (issue 110's first fault). And the moment a module for the
|
|
||||||
runtime exists, it is refused on every machine with the resolver, or, through the private network's
|
|
||||||
path, accepted without anyone noticing a collision.
|
|
||||||
|
|
||||||
## What resolves it
|
|
||||||
|
|
||||||
[ADR 0166](../../02-DECISIONS/0166-the-container-runtime-is-a-node-seat-and-the-host-creates-containers-through-its-holder.md)
|
|
||||||
gives the runtime a module that holds its seat and owns its file and service.
|
|
||||||
[ADR 0164](../../02-DECISIONS/0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md)
|
|
||||||
gives that module declared settings with defaults. The fix, once both are accepted:
|
|
||||||
|
|
||||||
1. The resolver module drops its runtime file and runtime service. It knows nothing of the runtime.
|
|
||||||
2. The private network stops generating either resource. ADR 0082's decision stands — being on the
|
|
||||||
network is what grants the trust, and no module author is involved — and only *who writes it*
|
|
||||||
moves. The mesh gives the registry to the runtime module as a value. ADR 0082 and ADR 0102 each
|
|
||||||
get a dated note saying where their mechanism now lives.
|
|
||||||
3. The runtime module writes `dns`, `live-restore` and `insecure-registries`, each a declared
|
|
||||||
setting with its cost: `dns` costs a restart, which `live-restore` makes harmless.
|
|
||||||
4. Steps 1–3 land in one push. A runtime module declaring the file beside a resolver module still
|
|
||||||
declaring it is refused.
|
|
||||||
5. The collision check sees a computed module's resources as well, so a second writer cannot come
|
|
||||||
back through generated code.
|
|
||||||
|
|
||||||
## Open questions
|
|
||||||
|
|
||||||
- **How the resolver's address reaches the runtime.** Either the resolver seat (`node-dns-resolver`)
|
|
||||||
delivers an address its holder serves, or the runtime module reads a machine fact and the seat
|
|
||||||
being held is only a precondition. The first tracks a resolver moving off the private address. The
|
|
||||||
second needs nothing new.
|
|
||||||
- **What `dns` defaults to on a machine with no resolver seat held.** Nothing, leaving the runtime's
|
|
||||||
own behaviour, is the honest default. A public resolver hides exactly the failure issue 110 took a
|
|
||||||
day to find.
|
|
||||||
- **The adopted machine's predecessor values.** The runtime module adopting a file with a
|
|
||||||
hand-written `dns` and `live-restore: false` replaces both. That is intended, and is the one
|
|
||||||
restart the operator must make on that machine.
|
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-10-02
|
||||||
|
located-in: [mesh-tools src/client.ts (toolsOn left node-scoped seats out of the listing), mesh-tools src/mcp.ts (the call resolved the key against that listing)]
|
||||||
|
fixed-by: mesh-tools 27 — node-scoped seats are listed with their scope, the verb requires the machine, and the call carries it in the subject
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 199 — A node-scoped seat's verb could not be called through the console
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
2026-10-02, the first time a node-scoped seat declared verbs
|
||||||
|
([ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md)). The packet filter's
|
||||||
|
holder on every machine served `rules`, `reload` and `remove` on the seat's per-machine subjects, and
|
||||||
|
the bus admitted them. The console answered every call with *nothing serves
|
||||||
|
node-packet-filter.rules@<machine>*.
|
||||||
|
|
||||||
|
[Design 33 §4](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) says a node-scoped seat's
|
||||||
|
tool carries the node it is asked of, as `<seat>.<verb>@<node>`. The console's listing left
|
||||||
|
node-scoped seats out — the comment said they *wait for a caller naming the node* — but the roles map
|
||||||
|
the console resolves a name against is built from that same listing. So the name never resolved as a
|
||||||
|
seat's verb, fell through to a module's subject nobody served, and the refusal named the wrong cause.
|
||||||
|
|
||||||
|
## Why it matters beyond this instance
|
||||||
|
|
||||||
|
A stated behaviour that did not happen, with a refusal that pointed elsewhere: the seat's verbs were
|
||||||
|
live on four machines and unreachable from the one surface a person uses. It could only be found by a
|
||||||
|
node-scoped seat declaring verbs, which none had.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-02
|
||||||
|
|
||||||
|
mesh-tools 27: node-scoped seats are listed with their scope, their verbs take a required `node`, the
|
||||||
|
call carries it in the subject, and a call without one is refused in words. Tested with a round trip
|
||||||
|
asking one machine's holder and being refused without a machine.
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-10-02
|
||||||
|
located-in: []
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 200 — The controller's answer to a long console call is refused by the bus
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
2026-10-02. A `push` of the control node asked through the console came back as *mesh-controller.push
|
||||||
|
did not answer in time. Something is serving it, so this is the tool being slow rather than absent.*
|
||||||
|
The push had run; the machine applied. The bus's log on the control node, at the same moment:
|
||||||
|
|
||||||
|
```
|
||||||
|
[ERR] 10.10.0.1:56030 - cid:4015 - Publish Violation - User "controller",
|
||||||
|
Subject "_INBOX.shanks.mesh-console.WRNO5V9IJ1FX35AU5NRBEB.WRNO5V9IJ1FX35AU5PN1UW"
|
||||||
|
```
|
||||||
|
|
||||||
|
The controller's reply to the console's request was refused: the controller's bus account may not
|
||||||
|
publish to the console's reply inbox. Shorter calls (`status`, `node`, `nodes`) answer; the long ones
|
||||||
|
(`push` of a large machine, `issue`) time out on the console's side although they succeed.
|
||||||
|
|
||||||
|
## Why it matters beyond this instance
|
||||||
|
|
||||||
|
A call that succeeds and is reported as a timeout sends a person to retry what already happened — a
|
||||||
|
second push, a second issue — and reads as the mesh being slow when it is the mesh refusing itself.
|
||||||
|
Whether the inbox prefix the console uses is the one the controller's account is allowed to answer, or
|
||||||
|
the request outlives the inbox subscription, is what a diagnosis has to tell apart.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- Which reply inboxes may the controller's account publish to, and which does the console request on?
|
||||||
|
- Does a reply after the requester's timeout count as a violation, or is the prefix itself refused?
|
||||||
Reference in New Issue
Block a user