Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ba68c154e | ||
|
|
460793af1c | ||
|
|
25de331e9b | ||
|
|
ed5ddcdef6 | ||
|
|
e4f80cc3ce | ||
|
|
d2689c0f86 | ||
|
|
719aa6bd62 | ||
|
|
ca13f59c88 | ||
|
|
f8a0402485 | ||
|
|
9016d88d54 | ||
|
|
6e5dfd2ab8 | ||
|
|
872f20d51f | ||
|
|
550453c5db | ||
|
|
b7aebedc2d | ||
|
|
27b2d30441 | ||
|
|
82fa5f79ea | ||
|
|
f6668d76d6 | ||
|
|
f5d54db7aa | ||
|
|
bcf010886d | ||
|
|
2eba399e1e | ||
|
|
d227ed12d2 | ||
|
|
8712d666bf | ||
|
|
61e70b9395 | ||
|
|
de032e704c | ||
|
|
0c2eae07c5 | ||
|
|
f23a71e0d7 | ||
|
|
9c13c89fa3 | ||
|
|
2db0ea268d | ||
|
|
8d83d94659 | ||
|
|
a8ffc2b94b | ||
|
|
1dcbdae1c4 | ||
|
|
c3ec48f85c | ||
|
|
72eda923c7 | ||
|
|
c4fedcdbe3 | ||
|
|
0bf70ee8b4 | ||
|
|
947b85af5e | ||
|
|
ac6c306df3 | ||
|
|
96df3ccc88 |
@@ -78,6 +78,12 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
|
|||||||
mesh-scoped exclusive claim is how the mesh says "there is one of me". A foundation seat is
|
mesh-scoped exclusive claim is how the mesh says "there is one of me". A foundation seat is
|
||||||
named after the server it guards: the `mesh-controller`, `postgres` and `lavinmq` modules claim
|
named after the server it guards: the `mesh-controller`, `postgres` and `lavinmq` modules claim
|
||||||
the `mesh-controller`, `mesh-store` and `mesh-broker` seats ([ADR 0079](../02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md)).
|
the `mesh-controller`, `mesh-store` and `mesh-broker` seats ([ADR 0079](../02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md)).
|
||||||
|
- **depends on a seat** — a module needing a seat held on its node by some module, without holding
|
||||||
|
it. Derived from the resources it declares, never stated: a `service` depends on
|
||||||
|
`node-service-manager`, a `package` on `node-package-manager`, a `container` on
|
||||||
|
`node-container-runtime` ([ADR 0207](../02-DECISIONS/0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md)).
|
||||||
|
Not a claim: a module **claims** a seat it holds and **declares** resources. Nothing claims a
|
||||||
|
package.
|
||||||
- **provision** — a service one module `provides` and others `require`; the mesh resolves a provider
|
- **provision** — a service one module `provides` and others `require`; the mesh resolves a provider
|
||||||
and wires the two with an endpoint and a credential. A provision is a service you offer, a seat
|
and wires the two with an endpoint and a credential. A provision is a service you offer, a seat
|
||||||
is a role you occupy, and the two meet where a seat delivers a provision: occupying the seat is
|
is a role you occupy, and the two meet where a seat delivers a provision: occupying the seat is
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
status: graduated
|
||||||
|
initiated: 2026-10-04
|
||||||
|
touches:
|
||||||
|
- 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/0126-a-module-declares-its-own-seats.md
|
||||||
|
- 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.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
|
||||||
|
- 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||||
|
- 03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md
|
||||||
|
- 03-DESIGN/01-to-be/37-the-operators-machine.md
|
||||||
|
- 03-DESIGN/01-to-be/38-building-the-operators-machine.md
|
||||||
|
- 04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md
|
||||||
|
became:
|
||||||
|
- 02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md
|
||||||
|
- 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md
|
||||||
|
- 02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md
|
||||||
|
- 03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 025 — How a module plugs into the operator's shell
|
||||||
|
|
||||||
|
## What is investigated
|
||||||
|
|
||||||
|
The shell module writes the mesh's part of the account's shell startup file. But the shell is not the
|
||||||
|
only module that needs a line there. A prompt theme loads itself from it. A language version manager
|
||||||
|
sets a variable and sources its loader. A toolchain puts its directory on `PATH`. A desktop module
|
||||||
|
names the browser. Today all of these sit in one hand-written file, and the shell module as written
|
||||||
|
carries some of them in its own block and loads others only "if a module placed them". Nothing says
|
||||||
|
how they get placed.
|
||||||
|
|
||||||
|
This effort asks four things:
|
||||||
|
|
||||||
|
1. **How a module contributes to the shell**: what it declares, who composes it, and in what order it
|
||||||
|
lands.
|
||||||
|
2. **Where the environment lives.** Variables and `PATH` entries are facts about the account, not
|
||||||
|
lines of one shell's syntax. They should reach every shell (interactive or not), the login shell's
|
||||||
|
`execute` verb, and programs a graphical session starts.
|
||||||
|
3. **Where the operator's own lines go,** so that assigning the shell module loses nothing the
|
||||||
|
machine does today.
|
||||||
|
4. **Which part of a file the mesh owns.** ADR 0174 calls the kept region the operator's; the host and
|
||||||
|
to-be 38 implement the inverse (the mesh owns a marked block, and everything outside it is the
|
||||||
|
operator's). The record this becomes says which.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Rolling out the shell module (to-be 38 WP5) was stopped on 2026-10-04 after a review of what assigning
|
||||||
|
it would do. Measured in [01](01-what-the-shell-file-holds-today.md):
|
||||||
|
|
||||||
|
- Every machine carries the same predecessor-written startup file, so the module's block would be
|
||||||
|
appended after its own older copy and everything would run twice.
|
||||||
|
- The block drops lines the machines rely on today.
|
||||||
|
- Nothing installs the prompt theme or the plugins the block loads.
|
||||||
|
- The `execute` verb runs a non-interactive login shell, which never reads the file the block is
|
||||||
|
written into.
|
||||||
|
|
||||||
|
The operator's direction: other modules must be able to plug themselves into the shell; the prompt
|
||||||
|
becomes its own module; assigning the shell module must lose no functionality; and the environment,
|
||||||
|
`PATH` above all, needs an answer of its own.
|
||||||
|
|
||||||
|
## What it touches
|
||||||
|
|
||||||
|
- The manifest. A contribution to the shell is either a new use of the existing `contributes` /
|
||||||
|
`receives` pair or a new gathered field like `jails` (to-be 31).
|
||||||
|
- The controller's composition, if the controller assembles the text.
|
||||||
|
- The `login-shell` seat (ADR 0176): what a holder must do with what is contributed to it, and
|
||||||
|
whether a module or the mesh declares the seat. Possibly a new seat for the environment, beside it
|
||||||
|
and beside the service manager's (ADR 0177). Research 023 asks the related question of a seat
|
||||||
|
naming the files its holder owns.
|
||||||
|
- ADR 0174's wording of the kept region, and ADR 0182's classification of the paths under a home.
|
||||||
|
- The zsh module, and the modules this makes possible: an environment module, the prompt, a version
|
||||||
|
manager, a toolchain.
|
||||||
|
|
||||||
|
## Where it stands
|
||||||
|
|
||||||
|
The operator proposed a separate **environment module**: one module, holding a mesh seat of its own,
|
||||||
|
that alone writes the account's environment. It writes a file that shells source and the service
|
||||||
|
manager's user environment, from the variables and `PATH` entries every other module contributes to
|
||||||
|
it. That is the starting position for the environment ([02](02-how-a-module-plugs-in.md) §1, option
|
||||||
|
E6). It leaves the shell's contribution as shell code only (§2), addressed to the `login-shell` seat,
|
||||||
|
which moves into the mesh's own seat set beside the new `node-environment` (§6).
|
||||||
|
|
||||||
|
Graduated on 2026-10-04 with one change from the starting positions: the controller, not the
|
||||||
|
environment module's own code, renders the environment into the module's files, so that the result
|
||||||
|
is in the declaration before a machine applies it ([ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md),
|
||||||
|
option 6b).
|
||||||
|
|
||||||
|
## Documents
|
||||||
|
|
||||||
|
- [01 — What the shell file holds today](01-what-the-shell-file-holds-today.md): evidence.
|
||||||
|
- [02 — How a module plugs in](02-how-a-module-plugs-in.md): the options and the starting position.
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
# 01 — What the shell file holds today
|
||||||
|
|
||||||
|
Measured 2026-10-04 on the four machines of one installation: two servers and two workstations. All
|
||||||
|
four have the account's login shell set to zsh, zsh installed from the distribution, and a
|
||||||
|
predecessor-written startup file. The predecessor is retired, so nothing manages these files any more.
|
||||||
|
|
||||||
|
## The startup file is the same everywhere
|
||||||
|
|
||||||
|
The account's `~/.zshrc` is **byte-identical on all four machines**: 102 lines, one checksum.
|
||||||
|
`~/.zshrc.local`, which the last line of `~/.zshrc` sources, comes in **two variants**: one shared by
|
||||||
|
both servers, and one shared by both workstations. So the "per-machine" part is really a
|
||||||
|
per-*kind*-of-machine part.
|
||||||
|
|
||||||
|
The predecessor produced these from one module with two *flavors*: a prompt flavor and an
|
||||||
|
autocomplete flavor, each of which swapped in a different local file. Its install hook also:
|
||||||
|
|
||||||
|
- cloned the prompt theme and three plugins from their upstream repositories into `~/.zsh/`;
|
||||||
|
- installed fonts;
|
||||||
|
- changed the login shell.
|
||||||
|
|
||||||
|
On the workstations the theme and plugins are still on disk, left over and now owned by nothing. The
|
||||||
|
servers have none of them.
|
||||||
|
|
||||||
|
## What the 102 lines are
|
||||||
|
|
||||||
|
Sorted by who should own each line once the machine is modules:
|
||||||
|
|
||||||
|
| Lines today | What they are | Natural owner |
|
||||||
|
|---|---|---|
|
||||||
|
| `EDITOR`, `VISUAL`, `XDG_CONFIG_HOME`, `PATH` gaining `~/.local/bin` and two script directories | the account's environment | the shell's default, or the environment itself |
|
||||||
|
| `PATH` gaining a toolchain's directory | environment, for one tool | the toolchain's module |
|
||||||
|
| a version manager's directory variable plus sourcing its loader | environment *and* shell code | the version manager's module |
|
||||||
|
| two variables naming the operator's own script library | environment, the operator's own | the operator |
|
||||||
|
| a variable that turns off an agent's terminal-title handling | environment, for one tool | the agent's module |
|
||||||
|
| the terminal title hook, keybindings, `dircolors`, the `ls`/`grep` aliases, `ll`/`la`/`l`, a container-run alias, two disk-usage functions, two port aliases | interactive shell behaviour | the shell's default |
|
||||||
|
| the prompt's instant-prompt cache, the theme, the prompt's own configuration file | shell code, order-sensitive (instant prompt first) | the prompt module |
|
||||||
|
| autosuggestions, syntax highlighting (and, unloaded, an autocomplete plugin on disk) | shell code, order-sensitive (syntax highlighting last) | a plugin module, or the prompt module |
|
||||||
|
| sourcing `~/.zshrc.local` | the operator's hook | the operator |
|
||||||
|
|
||||||
|
The workstation variant of the local file adds:
|
||||||
|
|
||||||
|
- more environment: a desktop toolkit theme, a file manager's plugin list, `BROWSER`, `VISUAL`
|
||||||
|
overridden to a graphical editor, a language toolchain's binary directory on `PATH`;
|
||||||
|
- two pieces of shell code: one that pads the prompt to the bottom of the terminal under a display, and
|
||||||
|
one that sources a function file another module places;
|
||||||
|
- a hook sourcing a further per-node file.
|
||||||
|
|
||||||
|
The server variant holds only that last module-placed source line.
|
||||||
|
|
||||||
|
**Count:** a workstation runs 65 non-comment lines from the two files (53 shared, 12 local); a server
|
||||||
|
runs 54. Of a workstation's 65:
|
||||||
|
|
||||||
|
- about a quarter (15) are environment;
|
||||||
|
- about half are interactive defaults no other module cares about;
|
||||||
|
- the remaining quarter is other modules' code and hooks (a prompt, plugins, a version manager, an
|
||||||
|
agent's functions), loaded from the shell file only because there was nowhere else to put it.
|
||||||
|
|
||||||
|
## The shell module as written
|
||||||
|
|
||||||
|
The `zsh` module of to-be 38 WP5 (catalogue change, unmerged):
|
||||||
|
|
||||||
|
- writes one block, appended at the end of `~/.zshrc`, holding a subset of the shared file:
|
||||||
|
- its environment lines, minus the toolchain directory, the version manager and the agent variable;
|
||||||
|
- the title hook, keybindings and the most common aliases, minus the port aliases;
|
||||||
|
- guarded `source` lines for the theme and two plugins *if present*;
|
||||||
|
- the source of `~/.zshrc.local`.
|
||||||
|
- assigned to any of the four machines, appends that block after the identical lines already there, so
|
||||||
|
every line in it runs twice, `~/.zshrc.local` included.
|
||||||
|
- on the servers, the guarded prompt lines find nothing; nothing installs the theme anywhere.
|
||||||
|
|
||||||
|
## Which startup file reaches what
|
||||||
|
|
||||||
|
zsh's startup order, and what each path through it reads:
|
||||||
|
|
||||||
|
| started as | reads |
|
||||||
|
|---|---|
|
||||||
|
| interactive login (a console, ssh with a terminal) | `.zshenv`, `.zprofile`, `.zshrc`, `.zlogin` |
|
||||||
|
| interactive non-login (a new terminal window) | `.zshenv`, `.zshrc` |
|
||||||
|
| non-interactive login: `zsh -lc …`, what the `execute` verb runs | `.zshenv`, `.zprofile`, `.zlogin`, **not** `.zshrc` |
|
||||||
|
| non-interactive: a script, `ssh host command` | `.zshenv` only |
|
||||||
|
|
||||||
|
So an environment written into `.zshrc` reaches neither `execute` nor a script. The distribution's
|
||||||
|
system-wide login profile, which zsh's system `zprofile` sources, only ever *appends* to `PATH` when an
|
||||||
|
entry is missing. An entry the account's `.zshenv` puts first therefore survives a login.
|
||||||
|
|
||||||
|
A graphical session's programs (a launcher, a bar, a window manager's key bindings) are started from the
|
||||||
|
display manager and the service manager, not from a shell, and read none of these files. The service
|
||||||
|
manager's own place for the account's environment is `~/.config/environment.d/`. Today it holds nothing
|
||||||
|
on any of the four machines, so a program launched from the window manager does not see `PATH` entries
|
||||||
|
that a terminal does.
|
||||||
|
|
||||||
|
## What the mesh already has for "many modules, one file"
|
||||||
|
|
||||||
|
Measured over the catalogue's 69 module definitions:
|
||||||
|
|
||||||
|
| mechanism | used by | shape |
|
||||||
|
|---|---|---|
|
||||||
|
| `contributes` / `receives` | 28 contribute, 15 receive | A consumer contributes **facts** keyed by a requirement. The provider receives all of them as one file in the mesh's own format, and **renders them itself**. "The controller does not know what a reverse proxy is." |
|
||||||
|
| `listens` / `filtering` | 40 declare listens, 1 composes | The controller derives the whole firewall rule set from every module's ports and writes it where the holder asks. |
|
||||||
|
| `jails` / `jailing` | 3 declare, 1 composes | Each module supplies its jail **in the tool's own format**. The controller assembles them, sorted, into the one file the holder names. |
|
||||||
|
| `into: block` on a file | 2 | One module's marked region inside a file something else owns. Text outside the region is kept byte for byte. Placement is at the end, or at the start. |
|
||||||
|
|
||||||
|
None of these is a contribution of shell code or of environment today.
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
# 02 — How a module plugs in
|
||||||
|
|
||||||
|
Six questions, taken one at a time: the environment, shell code, the operator's own lines, order,
|
||||||
|
who renders, and what a contribution is addressed to. Each has the options weighed and a starting
|
||||||
|
position. The positions were set with the operator on 2026-10-04 and are what this effort tests, not
|
||||||
|
what it has decided.
|
||||||
|
|
||||||
|
## 1. The environment: variables and `PATH`
|
||||||
|
|
||||||
|
A variable or a `PATH` entry is a fact about the account. It holds whichever shell is the login shell,
|
||||||
|
and it is wanted by:
|
||||||
|
|
||||||
|
- every shell, interactive or not;
|
||||||
|
- the login shell's `execute`;
|
||||||
|
- a graphical session's programs.
|
||||||
|
|
||||||
|
[01](01-what-the-shell-file-holds-today.md) measures that `.zshrc` reaches only the first kind, and
|
||||||
|
only interactively.
|
||||||
|
|
||||||
|
| | option | for | against |
|
||||||
|
|---|---|---|---|
|
||||||
|
| E1 | Each module writes lines into the shell's rc file (today) | nothing new | misses `execute`, scripts and the graphical session; written in one shell's syntax, so a second shell module starts over |
|
||||||
|
| E2 | A module contributes environment facts (a variable and its value; a `PATH` entry and its position) **to the login shell**. The holder renders them into its shell's always-read file (`~/.zshenv` for zsh) | reaches every zsh, `execute` included; a contributor names no path and no shell | the graphical session sees none of it; the environment is tied to which module holds the shell; every shell module reimplements the same rendering |
|
||||||
|
| E3 | E2, and the service-manager holder (ADR 0177) renders the same facts a second time into `~/.config/environment.d/` | the graphical session sees the same `PATH` as the terminal | one fact set, two owners, two renderings that can disagree; the service manager's module gains a duty unrelated to managing services |
|
||||||
|
| E4 | One composed file in `environment.d` syntax, sourced by the shell with export-all | one file, two readers | ties the shell to the service manager's syntax, which is close to POSIX assignments but not equal (its `${VAR:-default}` and quoting rules differ); a value with a space breaks one reader or the other |
|
||||||
|
| E5 | Shells take the environment from the service manager's environment generator, which prints the merged `environment.d` | no file of the shell's at all | every shell depends on the service manager and starts a process on every start; the generator's output is unquoted, so a value with a space breaks it |
|
||||||
|
| **E6** | **An environment module.** A module of its own (working name `node-env`) holds a mesh seat, `node-environment`, and is the only writer of the account's environment. Every module contributes its variables and `PATH` entries to that seat. The holder writes them in each reader's format: a POSIX file of `export` lines that shells source, and the service manager's `~/.config/environment.d/` | the environment no longer depends on which shell holds `login-shell`; one owner and one rendering per format, both from the same facts; the graphical session included without the service manager's module; a contributor addresses "the environment", never a shell; the `PATH` rules (order, de-duplication) live in one module's code, where a test can hold them | one more module and seat, assigned on every node beside the shell; the `login-shell` protocol gains a duty, to source the environment file, which must be written down and checked |
|
||||||
|
|
||||||
|
**Starting position: E6.** It was the operator's proposal on 2026-10-04, and it replaces this
|
||||||
|
document's first position (E2, then E3).
|
||||||
|
|
||||||
|
- The facts are the contribution. Each format is rendered once, by the one module whose subject is the
|
||||||
|
environment.
|
||||||
|
- A shell module's part shrinks to one line in its always-read file: `.zshenv` for zsh, sourcing the
|
||||||
|
environment module's POSIX file. A bash or fish module writes the same line in its own file, and no
|
||||||
|
contributor changes when the login shell does.
|
||||||
|
|
||||||
|
Sketched, for a node with zsh, the environment module, and a toolchain:
|
||||||
|
|
||||||
|
```
|
||||||
|
toolchain ──contributes PATH entry──▶ node-environment ◀──contributes EDITOR, ~/.local/bin── zsh
|
||||||
|
│ (held by node-env)
|
||||||
|
┌──────────────────┴──────────────────┐
|
||||||
|
▼ ▼
|
||||||
|
POSIX export file ~/.config/environment.d/
|
||||||
|
▲ ▲
|
||||||
|
sourced from ~/.zshenv read by the service manager
|
||||||
|
(every zsh, execute too) (the graphical session)
|
||||||
|
```
|
||||||
|
|
||||||
|
The shell module still contributes its own environment (`EDITOR`, `XDG_CONFIG_HOME`, `~/.local/bin` on
|
||||||
|
`PATH`) as a contributor like any other; it does not write those lines itself. Once issue 168 closes,
|
||||||
|
the values a person varies become settings of whichever module contributes them (ADR 0174).
|
||||||
|
|
||||||
|
## 2. Shell code: a prompt, plugins, a version manager's loader
|
||||||
|
|
||||||
|
This *is* one shell's syntax, and order matters: a prompt's instant-prompt cache must run first, and
|
||||||
|
syntax highlighting last.
|
||||||
|
|
||||||
|
| | option | for | against |
|
||||||
|
|---|---|---|---|
|
||||||
|
| S1 | **A contribution of code for one shell** (the shell it is for, the code, a slot), gathered by the controller and placed inside the holder's block in slot order. The same shape as `jails`, which a module supplies in fail2ban's own format and the controller assembles | a contributor names no path; the order is declared and checkable; unassigning the contributor removes its code at the next composition; a node holding fish simply has no zsh code rendered, and the resolver can say so | the controller gains one more gathered field; code for a shell travels in the declaration (in the clear, so no secrets in it, as for any file) |
|
||||||
|
| S2 | **A drop-in directory**: each module places its own `~/.zsh/rc.d/NN-name.zsh`, and the shell's block sources the directory | no controller change; each file is its module's own, removed when undeclared | every contributor hard-codes a path inside the shell module's territory, against ADR 0112's spirit; order is a naming convention nothing checks; nothing ties the file to the shell actually being zsh |
|
||||||
|
| S3 | Contributions as facts the holder renders (`contributes`/`receives` proper) | one mechanism with question 1 | code is not a fact; the holder would only paste it, which is S1 with an extra file |
|
||||||
|
|
||||||
|
**Starting position: S1.** A contribution to the shell carries **only code**, for named shells, each
|
||||||
|
piece in a slot. Variables and `PATH` entries never go here; they go to the environment (§1). So a
|
||||||
|
module touching both makes two contributions:
|
||||||
|
|
||||||
|
- A prompt module contributes zsh code in the first slot, and its own configuration file is its own
|
||||||
|
owned file (ADR 0182).
|
||||||
|
- A version manager contributes its directory variable to the environment, and its loader as code
|
||||||
|
for each shell it supports.
|
||||||
|
- A toolchain contributes a `PATH` entry to the environment and nothing to the shell.
|
||||||
|
|
||||||
|
What has to be settled: what each contribution is *addressed to*. Section 6 covers that.
|
||||||
|
|
||||||
|
## 3. The operator's own lines: the "local override"
|
||||||
|
|
||||||
|
Assigning the shell module must lose nothing the machine does today. That has two halves.
|
||||||
|
|
||||||
|
**What is common is the module's default, not an override.** The startup file is identical on all four
|
||||||
|
machines ([01](01-what-the-shell-file-holds-today.md)). A line every machine has is the shell module's
|
||||||
|
default, or another module's contribution. It is not a local override that a person would keep in step
|
||||||
|
on every machine by hand. Most of today's file therefore moves into the shell module's block and into
|
||||||
|
the contributions above. Little of it stays the operator's.
|
||||||
|
|
||||||
|
**What is the operator's is everything outside the mesh's block.** The host already works this way:
|
||||||
|
|
||||||
|
- the mesh's region is the marked block;
|
||||||
|
- text outside it is kept byte for byte, and checked unchanged;
|
||||||
|
- the region is given back when the module goes.
|
||||||
|
|
||||||
|
| | option | for | against |
|
||||||
|
|---|---|---|---|
|
||||||
|
| O1 | The mesh's block at the **start** of the file; the operator's lines after it | the operator's lines run last and win, which is what an override means; already supported (`at: start`) | a file the operator later rewrites must keep the markers; the host refuses a broken pair rather than guess |
|
||||||
|
| O2 | A named operator region *inside* a file the mesh writes whole (ADR 0174's wording) | the file is entirely the mesh's except one hole | the opposite of what the host implements; a file a person already owns becomes the mesh's |
|
||||||
|
| O3 | Only `~/.zshrc.local`, sourced from the block; `~/.zshrc` the mesh's whole | one obvious place | takes over a file the person owns today; ADR 0182 classifies the shell's own file as *written into*, not owned |
|
||||||
|
|
||||||
|
**Starting position: O1.** `~/.zshrc.local` keeps working because the operator's own lines source it,
|
||||||
|
not because the mesh's block does.
|
||||||
|
|
||||||
|
The record this effort becomes corrects ADR 0174's description of the kept region as a **progressive
|
||||||
|
insight**: the decision stands (a node varies a module by settings or by the operator's own lines,
|
||||||
|
never by an edit), and only its description of which side is marked changes.
|
||||||
|
|
||||||
|
**The one-off migration** is a person's act, listed in the module's documentation (ADR 0182):
|
||||||
|
|
||||||
|
- remove from today's file every line the block or a contribution now carries;
|
||||||
|
- keep the rest below the block.
|
||||||
|
|
||||||
|
Until a prompt module and the other contributors exist, the lines they will carry stay among the
|
||||||
|
operator's own. Nothing is lost at any step.
|
||||||
|
|
||||||
|
## 4. Order
|
||||||
|
|
||||||
|
Order matters only for code. The environment is set before any code runs, because zsh reads
|
||||||
|
`.zshenv` first. `PATH` entries carry their own position (before or after the system's), which the
|
||||||
|
environment module orders, not the shell.
|
||||||
|
|
||||||
|
| | option | for | against |
|
||||||
|
|---|---|---|---|
|
||||||
|
| R1 | Numbers (`10`, `50`, `90`) | familiar | every contributor guesses a number; collisions are silent |
|
||||||
|
| R2 | **A few named slots**, `first` / `normal` / `last`, with the module name breaking ties | the prompt says `first` and highlighting says `last` because that is what they mean; the composed result is the same bytes every time | three slots may not be enough |
|
||||||
|
|
||||||
|
**Starting position: R2.** Inside the shell module's block, the order is:
|
||||||
|
|
||||||
|
1. the line sourcing the environment module's file (in `.zshenv`, so it runs for every zsh; the rest
|
||||||
|
of this list is `.zshrc`);
|
||||||
|
2. the `first` slot;
|
||||||
|
3. the shell module's own defaults;
|
||||||
|
4. the `normal` slot;
|
||||||
|
5. the `last` slot.
|
||||||
|
|
||||||
|
The operator's lines come after the block, as option O1 says.
|
||||||
|
|
||||||
|
## 5. Who renders: the controller or the holder's code
|
||||||
|
|
||||||
|
There are two different renderings, and E6 lets them be answered differently.
|
||||||
|
|
||||||
|
**The environment** is facts rendered into two fixed formats by the one module whose subject they are.
|
||||||
|
|
||||||
|
- The environment module receives the gathered contributions (the `contributes` / `receives` shape:
|
||||||
|
facts in the mesh's own format, rendered by the receiver).
|
||||||
|
- Its own code writes the POSIX file and the `environment.d` file whenever what it receives changes.
|
||||||
|
That is ADR 0182's third class, written by the module's own process, owned by the account,
|
||||||
|
atomically.
|
||||||
|
- The controller learns no shell and no service manager. The `PATH` rules (prepend or append,
|
||||||
|
de-duplicate, keep the system's entries) are ordinary code with ordinary tests.
|
||||||
|
- To settle: what runs that code when the received file changes. The candidates are a host action
|
||||||
|
that restarts on the received file, or a subscription through the runtime (ADR 0198).
|
||||||
|
|
||||||
|
**Shell code** is not facts. It is text in the shell's own syntax, assembled in slot order, which is
|
||||||
|
what the controller already does for fail2ban jails: sort the pieces and concatenate them into the
|
||||||
|
holder's region. The controller assembles; it never interprets the code. This keeps the shell module
|
||||||
|
bundle-free for its files, and keeps the composed result visible in the declaration before a machine
|
||||||
|
applies it.
|
||||||
|
|
||||||
|
## 6. What a contribution is addressed to
|
||||||
|
|
||||||
|
Under E6 there are two addressees: the environment and the login shell.
|
||||||
|
|
||||||
|
| | option | for | against |
|
||||||
|
|---|---|---|---|
|
||||||
|
| A1 | **Seats**: environment facts to `node-environment`, shell code to `login-shell`. Each seat's protocol says what its holder does with what is contributed to it | a contributor depends on a role ("the environment", "the login shell"), never on zsh or on one module; works the same for any holder | `login-shell` today is declared by the zsh module itself (ADR 0126). A second shell module may only claim it, never declare it, and the seat exists only while zsh's definition is registered |
|
||||||
|
| A2 | Requirements the modules provide (`contributes` keyed by them, as the reverse proxy is) | an existing mechanism | a contributor on a node without the provider fails to resolve, though a toolchain's `PATH` entry with no environment module is merely unwritten |
|
||||||
|
|
||||||
|
**Starting position: A1, both seats in the mesh's own seat set** beside the service manager.
|
||||||
|
|
||||||
|
- `node-environment` is new, and is the mesh's from the start.
|
||||||
|
- `login-shell` moves there from the zsh module's definition. A shell is as universal a role as a
|
||||||
|
service manager, and a protocol that now carries duties (render the shell code contributed to it,
|
||||||
|
source the environment file) should not depend on one module's registration.
|
||||||
|
|
||||||
|
Research 023 (a seat's protocol naming what its holder owns) is the general form of this: the
|
||||||
|
environment seat would own the two environment files, and the login-shell seat the shell's
|
||||||
|
startup-file region. The two efforts should not decide it twice.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- Whether a contribution may be conditional on a capability: the workstation-only environment (a
|
||||||
|
browser, a toolkit theme) is a desktop module's contribution, which arrives only where that module is
|
||||||
|
assigned. Measured, this may need nothing new.
|
||||||
|
- What a node without the environment module does with environment contributions: refuse them at
|
||||||
|
resolve, or leave them unwritten and say so. The position here is to say so; a missing `PATH` entry
|
||||||
|
is a visible gap, not a broken machine.
|
||||||
|
- Whether the operator's own variables (the script-library paths in [01](01-what-the-shell-file-holds-today.md))
|
||||||
|
are the operator's lines below the shell block, or a kept region of the environment module's file.
|
||||||
|
The first needs nothing new, but reaches only interactive zsh.
|
||||||
|
- How the prompt module and a plugin module divide the plugins. Packaging decides it as much as
|
||||||
|
ownership: the plugins come from upstream repositories, not distribution packages, on these machines.
|
||||||
|
- Whether the `execute` verb should read the interactive file at all once the environment is in
|
||||||
|
`.zshenv`. The position here is no: a non-interactive login shell plus the environment is what a
|
||||||
|
command needs, and the prompt's code should not run for it.
|
||||||
|
- How a contribution reaches a second shell assigned beside the holder, which to-be 38 WP5 names as the
|
||||||
|
first follow-up record. Under A1 a non-holder renders nothing, so the question becomes whether a
|
||||||
|
non-holding shell module may render contributions for interactive use.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
status: active
|
||||||
|
initiated: 2026-10-04
|
||||||
|
touches:
|
||||||
|
- 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/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
|
||||||
|
- 02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md
|
||||||
|
- 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md
|
||||||
|
- 02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md
|
||||||
|
- 03-DESIGN/01-to-be/37-the-operators-machine.md
|
||||||
|
- 03-DESIGN/01-to-be/38-building-the-operators-machine.md
|
||||||
|
- 04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md
|
||||||
|
became: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# 026 — The graphical session as modules
|
||||||
|
|
||||||
|
## What is investigated
|
||||||
|
|
||||||
|
The workstations' graphical session as modules of the mesh, at the same level as the shell
|
||||||
|
([to-be 41](../../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md)): a package, files
|
||||||
|
under the account's home, a seat, and nothing that names a machine. The pieces are:
|
||||||
|
|
||||||
|
- the login manager;
|
||||||
|
- how a session starts and what environment it gets;
|
||||||
|
- the display server (X today, Wayland as a sibling);
|
||||||
|
- the window manager (i3, and sway as its Wayland sibling);
|
||||||
|
- the terminal emulator (xterm);
|
||||||
|
- the session's companions: bar, compositor, launcher, notifier, lock and idle, clipboard,
|
||||||
|
wallpaper, theming, fonts.
|
||||||
|
|
||||||
|
[To-be 38](../../03-DESIGN/01-to-be/38-building-the-operators-machine.md) names this WP7, and says
|
||||||
|
each seat begins with a record naming its holders and verbs. [To-be 37](../../03-DESIGN/01-to-be/37-the-operators-machine.md)
|
||||||
|
§4 leaves one question for the resolver: whether a held seat can gate another's assignment.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The operator asked for the graphical modules next, at the shell's level, and for one consistent
|
||||||
|
experience across machines. Since the predecessor retired, nothing manages the workstations'
|
||||||
|
desktops. Measured in [01](01-what-the-workstations-run.md):
|
||||||
|
|
||||||
|
- Two workstations carry one 983-line predecessor module's output, still byte-identical in its core.
|
||||||
|
- One workstation also carries another machine's hardware fragments.
|
||||||
|
- One runs a session that predates two fixes, with two notification daemons and two portals.
|
||||||
|
- The session's environment is a hand-kept second copy of the account's, beside the one the mesh
|
||||||
|
now writes.
|
||||||
|
|
||||||
|
## How it is approached
|
||||||
|
|
||||||
|
**Adopting is also improving** (the operator, 2026-10-04). A module is not a copy of what a machine
|
||||||
|
does today. Making it is the moment to fix what is broken, drop what is dead, choose the better tool
|
||||||
|
and remove the leftovers. Every module's design lists its improvements over today. **Every module
|
||||||
|
also serves tools,** many of them, for reading, acting and diagnosing; a module that only places a
|
||||||
|
package and a file is unfinished. The tools are catalogued in
|
||||||
|
[026/05](../026-the-graphical-session-as-modules/05-the-tools-each-module-serves.md).
|
||||||
|
|
||||||
|
## What it touches
|
||||||
|
|
||||||
|
- **The seat table:** up to ten node seats.
|
||||||
|
- **The resolver:** a seat held on a node gating another module's assignment.
|
||||||
|
- **The contribution mechanism of ADR 0204:** whether it generalises beyond shells, or whether
|
||||||
|
tools' own drop-in directories serve.
|
||||||
|
- **The host's user-scoped units** (mesh-host #72, still open).
|
||||||
|
- **Settings** for per-machine values (issue 168).
|
||||||
|
- **ADR 0205's archive** for the two pieces the distribution does not package.
|
||||||
|
|
||||||
|
## Documents
|
||||||
|
|
||||||
|
- [01 — What the workstations run](01-what-the-workstations-run.md): evidence.
|
||||||
|
- [02 — The questions and the options](02-the-questions-and-the-options.md)
|
||||||
|
- [04 — Screensaver, displays and menus](04-screensaver-displays-and-menus.md): the lock and idle module, monitor layouts by the monitors' identity, rofi and dmenu behind one launcher seat, the clipboard, fonts
|
||||||
|
- [05 — The tools each module serves](05-the-tools-each-module-serves.md): a first catalogue for the modules of 026 and 027
|
||||||
|
- [03 — What the predecessor taught](03-what-the-predecessor-taught.md): its 128 modules and 3,395 commits, as patterns to keep and failures not to repeat; shared with research 027.
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# 01 — What the workstations run
|
||||||
|
|
||||||
|
Measured 2026-10-04 on the two workstations of one installation, read-only: a laptop with a hybrid
|
||||||
|
GPU and an internal panel, and a desktop with one GPU and two external monitors. Both run the same
|
||||||
|
predecessor-generated desktop. File equality was checked by checksum across the two machines.
|
||||||
|
|
||||||
|
## How a session starts
|
||||||
|
|
||||||
|
The chain is the same on both:
|
||||||
|
|
||||||
|
1. The login manager (`lemurs`, built from the distribution's user repository, its package now in
|
||||||
|
the official one) runs its X setup script on a virtual terminal.
|
||||||
|
2. That script sources the login shell's profile files, then `~/.xprofile`, then the system's
|
||||||
|
`xinitrc.d` drop-ins, then merges `~/.Xresources`.
|
||||||
|
3. `~/.xprofile` reuses the systemd user manager's bus, then sources `~/.xinitrc`.
|
||||||
|
4. `~/.xinitrc` sets up the session and ends with `exec i3`.
|
||||||
|
|
||||||
|
The login manager's own window-manager entry (`exec startx`) is never reached. Its configuration
|
||||||
|
file uses a format two releases old, and an unmerged newer one sits beside it.
|
||||||
|
|
||||||
|
**What `~/.xinitrc` does**, in order:
|
||||||
|
|
||||||
|
1. Sources the system drop-ins, which import `DISPLAY` and `XAUTHORITY` into the user manager.
|
||||||
|
2. Starts the keyring and exports its ssh socket.
|
||||||
|
3. Exports the session's environment:
|
||||||
|
- `PATH`, with nine entries, one of them a directory that no longer exists;
|
||||||
|
- toolchain variables;
|
||||||
|
- `XDG_CONFIG_HOME` and `XDG_DATA_DIRS` (with flatpak);
|
||||||
|
- five GTK/Qt theme variables;
|
||||||
|
- the desktop's identity (`XDG_CURRENT_DESKTOP`, `XDG_SESSION_DESKTOP`);
|
||||||
|
- three of the operator's own variables.
|
||||||
|
4. Imports an explicit allowlist of ten of those into the user manager and D-Bus activation. It is
|
||||||
|
never `--all`, because:
|
||||||
|
5. a predecessor file of **secrets as environment variables** (package-registry and API tokens) is
|
||||||
|
sourced next.
|
||||||
|
6. Sets the screensaver and display power timeouts, restores the wallpaper, and starts the lock
|
||||||
|
watcher in a respawn loop. It is deliberately not a unit, because it needs the login session.
|
||||||
|
7. `exec i3`.
|
||||||
|
|
||||||
|
**The account's environment, as of today, has three sources that disagree:**
|
||||||
|
|
||||||
|
- this file, for the session;
|
||||||
|
- the mesh's `environment.sh`, for shells
|
||||||
|
([ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md));
|
||||||
|
- `~/.config/environment.d/`, for the user manager. It holds the mesh's `50-mesh.conf`, and a
|
||||||
|
predecessor file that **sets `PATH` outright** and sorts after it.
|
||||||
|
|
||||||
|
## The roles, and what fills them
|
||||||
|
|
||||||
|
| role | software | where configured |
|
||||||
|
|---|---|---|
|
||||||
|
| login manager | lemurs | `/etc/lemurs/*` (identical on both, and to the predecessor's source) |
|
||||||
|
| session start and environment | the login manager's X setup, `~/.xprofile`, `~/.xinitrc`, `xinitrc.d`, the D-Bus import, `environment.d` | `~/.xprofile`, `~/.xinitrc`, `~/.config/environment.d/*` |
|
||||||
|
| display server | Xorg (`xorg-server`, `xinit`, the X apps; vendor drivers per GPU) | **no** `xorg.conf.d`; monitors by `xrandr` scripts |
|
||||||
|
| monitor layout | `xrandr` scripts (arandr), a hotplug rule on the laptop | `~/.screenlayout/`, a scripts folder, a window-manager fragment |
|
||||||
|
| window manager | i3 4.25 | `~/.config/i3/config` and `config.d/*`, a reload watcher (user unit) |
|
||||||
|
| bar | i3bar with i3status-rust | `~/.config/i3status-rust/*`, 14 themes, a bar watchdog (user unit) |
|
||||||
|
| terminal | xterm (the only terminal installed) | `~/.Xresources.d/xterm`, the window manager's binding, the compositor's opacity rule |
|
||||||
|
| compositor | picom | `~/.config/picom/picom.conf` |
|
||||||
|
| launcher and menus | rofi | `~/.config/rofi/*`, launcher, power-menu and theme-picker scripts |
|
||||||
|
| notifier | dunst (D-Bus activated) | `~/.config/dunst/dunstrc`, `dunstrc.d/*` |
|
||||||
|
| lock, idle, display power | xss-lock and i3lock-color, `xset` | `~/.xinitrc`, a lock script |
|
||||||
|
| clipboard | greenclip, xclip | `greenclip.toml` |
|
||||||
|
| wallpaper | feh | `~/.fehbg` (points into the predecessor's tree) |
|
||||||
|
| theming | Adwaita dark, qt5ct/qt6ct, the desktop portal (GTK backend pinned) | GTK `settings.ini`, `qt*ct.conf`, `portals.conf`, an appearance script, `.Xresources` cursor |
|
||||||
|
| fonts | Hack and Meslo Nerd fonts in `~/.local/share/fonts` (not packaged), noto | `~/.Xresources.d/xft` (DPI fixed at 96) |
|
||||||
|
| keyboard | nothing set; the default layout; vendor keys via triggerhappy on the laptop | window-manager bindings, `/etc/triggerhappy` |
|
||||||
|
|
||||||
|
**Packages:** every piece except two is in the distribution's official repositories, and the login
|
||||||
|
manager now is too. The two exceptions are the lock screen's colour build (`i3lock-color`) and the
|
||||||
|
clipboard manager (`rofi-greenclip`). The Nerd fonts exist as official packages, but both machines
|
||||||
|
carry hand-copied files instead.
|
||||||
|
|
||||||
|
## Identical, different, and why
|
||||||
|
|
||||||
|
**Byte-identical on both machines:**
|
||||||
|
|
||||||
|
- the session files: `.xinitrc`, `.xprofile`, `.Xresources` and its drop-ins;
|
||||||
|
- the i3 main configuration and two of its fragments;
|
||||||
|
- the bar's top configuration and themes;
|
||||||
|
- picom, rofi, the GTK and Qt settings, the portal configuration, the login manager.
|
||||||
|
|
||||||
|
**Different, by cause:**
|
||||||
|
|
||||||
|
| cause | what |
|
||||||
|
|---|---|
|
||||||
|
| hardware | the monitor layout script; the bar's battery block; the laptop's power and vendor-key units and udev rules |
|
||||||
|
| misassignment | the desktop carries the **laptop's** hardware fragments: the vendor-key daemon and its triggers, the backlight rule, the brightness drop-in, a touchpad reset, and the laptop's monitor layouts, in an older version |
|
||||||
|
| drift | the notifier's position and corner radius; a "temporary" window-manager fragment from a test; the bar watchdog disabled; a second Qt configuration tool; different font builds |
|
||||||
|
| a stale session | the desktop's session began before two fixes, so it runs two notification daemons and two portals, and its user manager lacks the desktop's identity |
|
||||||
|
|
||||||
|
**Dead references:** the window manager starts a polkit agent that is installed on neither machine,
|
||||||
|
so there is no polkit agent at all. `PATH` names a directory that does not exist.
|
||||||
|
|
||||||
|
**Per-machine values inside shared files:**
|
||||||
|
|
||||||
|
- the DPI;
|
||||||
|
- absolute home paths, in the clipboard configuration and the flatpak data directories;
|
||||||
|
- the laptop's panel name, inside a fragment both machines carry.
|
||||||
|
|
||||||
|
## User units the desktop needs
|
||||||
|
|
||||||
|
| unit | does | laptop | desktop |
|
||||||
|
|---|---|---|---|
|
||||||
|
| reload watcher | reloads the window manager and bar when their files change | on | on |
|
||||||
|
| bar watchdog | restarts a dead bar | on | off |
|
||||||
|
| clipboard daemon | from its package | via the window manager | unit **and** window manager |
|
||||||
|
| vendor power profile, memory guard | laptop power | on | — |
|
||||||
|
|
||||||
|
None is managed. Applying them as the account needs the host's user scope
|
||||||
|
([ADR 0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)),
|
||||||
|
which is still an open change.
|
||||||
|
|
||||||
|
## The predecessor's module
|
||||||
|
|
||||||
|
One manifest of 983 lines covers the window manager, bar, launcher, notifier, compositor, lock
|
||||||
|
screen, session bootstrap, theming and scripts. It:
|
||||||
|
|
||||||
|
- has four *flavors*: i3, laptop (i3 plus the monitor wizard and hotplug), desktop (i3 plus
|
||||||
|
nothing) and a laptop model (laptop plus vendor keys);
|
||||||
|
- has about **105 theme variables** substituted into templates: border, gaps, fonts, workspace
|
||||||
|
names, every colour of bar, launcher, notifier and lock screen, compositor opacity, cursor, idle
|
||||||
|
times, Qt and GTK theme names;
|
||||||
|
- enables the two user units from an install hook.
|
||||||
|
|
||||||
|
Separate modules held the login manager and the display server (one flavor, `xorg`, with a comment
|
||||||
|
calling `wayland` "the intended sibling"). The shell module held no graphical part.
|
||||||
|
|
||||||
|
## Wayland and sway
|
||||||
|
|
||||||
|
**Nothing exists.** There is no compositor, no sway configuration, no Wayland session entry, and the
|
||||||
|
login manager's Wayland directory is empty. What is installed is libraries:
|
||||||
|
|
||||||
|
- Wayland itself and the Qt Wayland plugins, which other packages pull in;
|
||||||
|
- `xwayland`, explicitly installed and required by nothing;
|
||||||
|
- on the desktop, an orphaned compositor library from another desktop environment, and that
|
||||||
|
environment's portal backend, pulled in by a game launcher. The portal configuration pins
|
||||||
|
against it.
|
||||||
|
|
||||||
|
Every piece a sway session needs is in the official repositories: the compositor, its lock screen,
|
||||||
|
a terminal (`foot`), a bar (`waybar`), a notifier (`mako`) and `xwayland`.
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# 02 — The questions and the options
|
||||||
|
|
||||||
|
Seven questions. Each has its options and a starting position, which is what this effort tests, not
|
||||||
|
what it has decided.
|
||||||
|
|
||||||
|
## 1. How finely the desktop splits into modules
|
||||||
|
|
||||||
|
| | option | for | against |
|
||||||
|
|---|---|---|---|
|
||||||
|
| G1 | One desktop module, as the predecessor had | one assignment | flavors again, per machine; ADR 0174 refuses them, and the evidence shows a flavor landing on the wrong machine |
|
||||||
|
| G2 | **One module per piece of software:** `lemurs`, `xorg`, `i3`, `i3status-rust`, `xterm`, `picom`, `rofi`, `dunst`, `xss-lock` with the lock screen, `greenclip`, `feh`, a theme module, a fonts module | each is what it declares; a machine gets exactly what is assigned; the same split already works for the shell and its plugins | about thirteen assignments per workstation |
|
||||||
|
| G3 | G2, plus a named **set** the controller assigns as one (for example *the X desktop*) | G2's precision with G1's convenience | a set is a new controller concept |
|
||||||
|
|
||||||
|
**Starting position: G2.** Whether a set is worth a record is left until the thirteen assignments
|
||||||
|
have been done by hand once.
|
||||||
|
|
||||||
|
## 2. The seats
|
||||||
|
|
||||||
|
Research 018 listed the candidates. ADR 0204 has since put the login shell in the mesh's own set,
|
||||||
|
because a role with a protocol should not depend on one module's registration. The same reasoning
|
||||||
|
applies here:
|
||||||
|
|
||||||
|
| seat | holders | protocol, first verbs |
|
||||||
|
|---|---|---|
|
||||||
|
| `node-login-manager` | lemurs, greetd | which sessions it offers, the default session |
|
||||||
|
| `node-display-server` | xorg, sway | `displays`, `layout` |
|
||||||
|
| `node-display-session` | i3, sway | `reload`, `workspaces`, `windows` |
|
||||||
|
| `node-terminal-emulator` | xterm, foot, alacritty | which terminal `$TERMINAL` names; `open` |
|
||||||
|
| `node-bar`, `node-compositor`, `node-launcher`, `node-notifier`, `node-lock-screen`, `node-clipboard` | the pieces above, and their Wayland counterparts | one verb or none each, until a use asks for one |
|
||||||
|
|
||||||
|
**A compositor that is its own server holds two seats.** Sway is both the display server and the
|
||||||
|
display session. A module may claim several seats, so this needs nothing new.
|
||||||
|
|
||||||
|
**Starting position:** the first four seats are in the mesh's own set. The companion seats are
|
||||||
|
added only as each holder is written; for those, a module without a seat is acceptable at first.
|
||||||
|
|
||||||
|
## 3. One module requiring another seat to be held
|
||||||
|
|
||||||
|
i3 needs an X server held on its node, and sway needs nothing below it. A terminal needs a session.
|
||||||
|
To-be 37 left open how that is said.
|
||||||
|
|
||||||
|
| | option | for | against |
|
||||||
|
|---|---|---|---|
|
||||||
|
| R1 | A seat **delivers a provision** (`x11-display`, `wayland-display`) and a module requires it at node scope. The seat table has a `delivers` field already, and requirements already resolve | existing machinery; the refusal names the seat and its possible holders, which design 27 already lists | a node-scoped requirement that never crosses machines has to be stated as such |
|
||||||
|
| R2 | A new field, *needs the seat X held* | reads plainly | a second way to say what R1 says |
|
||||||
|
| R3 | Nothing; assign carefully | — | the mistake the evidence shows (a laptop's fragments on a desktop) is exactly an unchecked assignment |
|
||||||
|
|
||||||
|
**Starting position: R1.** `xorg` and `sway` each deliver what they serve. `i3`, `picom` and `xss-lock`
|
||||||
|
require `x11-display`. `foot` requires a Wayland display, and xterm requires an X one, which a Wayland
|
||||||
|
session gives through `xwayland`.
|
||||||
|
|
||||||
|
## 4. Who starts the session, and with what environment
|
||||||
|
|
||||||
|
Today `~/.xinitrc` is a hand-kept second environment and the session's whole start script.
|
||||||
|
|
||||||
|
| | option | for | against |
|
||||||
|
|---|---|---|---|
|
||||||
|
| S1 | The display server's module writes `~/.xinitrc` **into**: a mesh block at the start that sources the account's environment (`environment.sh`), merges the X resources, and runs the session's contributed start lines. The session holder's module contributes its `exec` line. The operator's lines stay after the block | one environment for shells, the session and the user manager; nothing to keep in step | the order inside `.xinitrc` becomes the slot order of a contribution (question 5) |
|
||||||
|
| S2 | The login manager's module owns the session script under `/etc` | system scope; no home file | the environment is the account's, and the script is the same for every account |
|
||||||
|
| S3 | Leave `.xinitrc` the operator's | nothing to build | the third environment stays |
|
||||||
|
|
||||||
|
**Starting position: S1.**
|
||||||
|
|
||||||
|
- The desktop's identity (`XDG_CURRENT_DESKTOP`) and the theme variables become **environment
|
||||||
|
contributions** (ADR 0203) from `i3` and from the theme module. They then also reach the user
|
||||||
|
manager through `environment.d`, which replaces most of today's allowlist import.
|
||||||
|
- The secrets file stays out of the environment until research 027 settles how a secret reaches an
|
||||||
|
account.
|
||||||
|
|
||||||
|
## 5. How other modules contribute to a holder's file
|
||||||
|
|
||||||
|
The terminal's settings are X resources. A bar, a launcher binding and a hardware module's key
|
||||||
|
bindings are window-manager configuration. Autostarts are the session's. ADR 0204 built slot
|
||||||
|
contributions for shells only.
|
||||||
|
|
||||||
|
| | option | for | against |
|
||||||
|
|---|---|---|---|
|
||||||
|
| C1 | **The tool's own drop-in directory**, where it has one: i3's `include`, dunst's `dunstrc.d`, X resources' `#include`, XDG autostart entries, `environment.d`. Each contributor owns its own file there | no mesh change; the tools already read these directories; unassigning removes the file | each contributor names a path in another tool's directory (ADR 0204 rejected this for shells, where no drop-in convention exists); ordering is by file name |
|
||||||
|
| C2 | **ADR 0204's mechanism generalised:** `contributes` text *for a format* (`zsh`, `xresources`, `i3`, `xinitrc`) in a slot, placed by the holder's placeholder | one mechanism, checked by the controller, order declared | every format must be named in the controller; a bigger change to ADR 0204 |
|
||||||
|
| C3 | C1 where the tool has a drop-in convention, C2 where it does not (`.xinitrc`, `.Xresources` order) | uses each tool's own grain | two mechanisms to learn |
|
||||||
|
|
||||||
|
**Starting position: C3**, with the boundary drawn by the tools. A tool that reads a directory gets
|
||||||
|
drop-ins. A file without one gets slots. This means amending ADR 0204's "shell" to "a format", which
|
||||||
|
is a progressive extension rather than a reversal.
|
||||||
|
|
||||||
|
## 6. What varies per machine
|
||||||
|
|
||||||
|
| what | today | option |
|
||||||
|
|---|---|---|
|
||||||
|
| monitor layout | per-machine `xrandr` scripts, monitor names baked in | a **setting** of `xorg` (issue 168), and a `layout` verb of the display server seat |
|
||||||
|
| DPI, fonts' size | fixed in an X resource | a setting |
|
||||||
|
| battery block, vendor keys, brightness, touchpad | a laptop model's flavor | **a hardware module** per machine model, contributing its window-manager fragment, bar block and udev rules. The desktop simply is not assigned it |
|
||||||
|
| theme (the 105 variables) | template substitution | settings of each tool's module, after issue 168 closes (ADR 0174). Until then each module carries today's values as its default |
|
||||||
|
|
||||||
|
**Starting position:**
|
||||||
|
|
||||||
|
- Hardware modules for what follows the machine.
|
||||||
|
- Defaults now, settings after issue 168, for what the operator varies.
|
||||||
|
- The monitor layout waits for settings. Until then it is an operator-owned script the display
|
||||||
|
server's block calls if present.
|
||||||
|
|
||||||
|
## 7. Wayland and sway
|
||||||
|
|
||||||
|
Nothing of a Wayland session exists, and every piece is officially packaged. "Wayland" is a protocol,
|
||||||
|
not a piece of software, so it has no module of its own. Its parts are `sway` (server and session),
|
||||||
|
`swaylock`, `foot`, `waybar`, `mako`, and `xwayland` for X clients.
|
||||||
|
|
||||||
|
**Starting position:**
|
||||||
|
|
||||||
|
- The seats and the requirements (questions 2 and 3) are designed so that sway fits from the first
|
||||||
|
day.
|
||||||
|
- The X stack is built first, because it is what runs.
|
||||||
|
- `sway` and its companions are written after that, and proven on one workstation as a second
|
||||||
|
session the login manager offers beside i3. That lets the operator try it without losing the
|
||||||
|
working desktop.
|
||||||
|
|
||||||
|
## Prerequisites this effort cannot remove
|
||||||
|
|
||||||
|
- **User-scoped units** (mesh-host #72) for the reload watcher and the bar watchdog.
|
||||||
|
- **Settings** (issue 168) for monitors and theme values.
|
||||||
|
- **The two packages not in the official repositories:** the lock screen's colour build and the
|
||||||
|
clipboard manager. Each is ADR 0205's case, a pinned archive, or a choice of an official
|
||||||
|
alternative (`i3lock` without colours; `clipmenu`/`cliphist`).
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# 03 — What the predecessor taught
|
||||||
|
|
||||||
|
A study on 2026-10-04 of the retired predecessor:
|
||||||
|
|
||||||
|
- its 128 module manifests, their hooks, its installer and its sync engine;
|
||||||
|
- 3,395 commits of history;
|
||||||
|
- what it left on four machines.
|
||||||
|
|
||||||
|
This document holds what bears on the graphical session and on the system layer
|
||||||
|
([research 027](../027-the-system-layer-as-modules/00-overview.md)). The evidence is in the
|
||||||
|
predecessor's history. A commit is cited here by what it fixed, not by its hash, because the
|
||||||
|
repository is private.
|
||||||
|
|
||||||
|
## Keep: what worked
|
||||||
|
|
||||||
|
| pattern | where it shows | in the mesh |
|
||||||
|
|---|---|---|
|
||||||
|
| ownership marked inside the file: inside the markers is reconciled, outside is kept verbatim | a block marker in a shared file, after an engine that rewrote whole files | kept regions (ADR 0174, ADR 0204) |
|
||||||
|
| two writers get two files and an `include`, the include first | the ssh client's configuration, after two writers fought over one file | ADR 0203's two files; research 026 C1 |
|
||||||
|
| one writer per file, one authority per action | only the reload watcher restarts the window manager, after three mechanisms each did | ADR 0182 |
|
||||||
|
| refuse to write when the source of truth is unreadable; never empty a block because a query found nothing | a block of names was emptied by a failed query | — keep |
|
||||||
|
| an unresolved template variable fails the install | a literal unfilled path was installed green | [issue 231](../../04-ISSUES/231-a-misspelled-placeholder-is-written-out-as-text/00-report.md): the mesh still has this gap |
|
||||||
|
| prune only what you can prove you placed | stale files from earlier deliveries | ADR 0189 |
|
||||||
|
| ensuring never rotates a credential | a silent rotation caused a retry storm, a ban of the shared address and a lost registry | ADR 0114 |
|
||||||
|
| vendor only the files you use; never clone and link | three files instead of 77 MB | ADR 0205 |
|
||||||
|
| copy, never symlink | a recursive delete followed a link, and every reinstall failed | ADR 0012 |
|
||||||
|
| verification says what it did not check | a verifier said *clean* while the secret was still on disk | — keep |
|
||||||
|
| alert once per condition | 411 alerts hid a 28-hour outage | ADR 0090 |
|
||||||
|
|
||||||
|
## Do not repeat
|
||||||
|
|
||||||
|
| failure | what it did | the mesh instead | where the mesh is still exposed |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Flavors** | variant files and packages per machine type: a gate dropped, the first-seen variant won, packages never installed, the verifier ignored the gate. On the day of the study a desktop carried a laptop model's fragments | one module per piece, assignment per machine (ADR 0174, research 026 §1) | a setting that switches which whole file is rendered is a flavor under another name |
|
||||||
|
| **The freeze** | existing values outranked new defaults; templated files were rendered once (*copy if absent*) | files are generated (ADR 0011) | a created-once file (ADR 0087) is a deliberate freeze, and a push must say *kept* |
|
||||||
|
| **Adopting drift** | a *merge* strategy made a local edit the record forever; switching strategies clobbered a person's model choice | nothing is read back (ADR 0174) | an edit outside a kept region is overwritten **silently**. The predecessor's *why is this back* loop: the push should name what it overwrote |
|
||||||
|
| **Environment templating** | `${VAR}` matched any name; unresolved names stayed literal; comments and destination paths were interpolated | namespaced placeholders; `$` forbidden in contributed values (ADR 0203) | issue 231 |
|
||||||
|
| **Hooks with privilege** | install hooks ran `sudo`, `chsh`, `systemctl`, `git clone` and `curl`, and swallowed failures into a warning | the `user` shape, the service shape, archives (ADR 0176, 0177, 0205) | the agent module writes under `/etc` from its own tool through `sudo` (no keep-original, no give-back); the prompt's helper downloads itself unpinned; that the operator escalates without a prompt is assumed by three modules and declared by none (research 027) |
|
||||||
|
| **Secrets in environment files** | `.env` files left world-readable; the decryption key beside what it decrypts; a deleted secret stayed in the file, so rotation was a no-op | the vault (ADR 0113, 0114) | a predecessor file of secrets is still sourced into the graphical session on two machines (research 027 Q2) |
|
||||||
|
| **Symlinks into a home** | a system file linked into a person's home | ADR 0012 | on the control machine, a fail2ban action file is still a predecessor link into its home tree. Deleting that tree would silently break the repeat-offender jail. The mesh's fail2ban module must own it as a file first |
|
||||||
|
| **Green while broken** | a recorded version frozen for four months; a verifier passing what it skipped | ADR 0134, 0145, 0184 | issue 230: a plan waiting for ever reads as healthy |
|
||||||
|
|
||||||
|
## What it means here
|
||||||
|
|
||||||
|
- **Research 026:** the desktop's 88 flavor-gated files and 92 theme variables are the flavor and
|
||||||
|
templating failures in one module. Question 1 (one module per piece) and question 6 (hardware
|
||||||
|
modules, settings later) are the answer, and nothing in the new modules may switch whole files on a
|
||||||
|
setting.
|
||||||
|
- **Research 027:** the hooks that installed the AUR helper, enabled the login manager and changed
|
||||||
|
shells are what the `package`, `service` and `user` shapes replace. Every remaining `sudo` in a module's
|
||||||
|
own code is a debt to be named, starting with the agent module.
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
# 04 — Screensaver, displays and menus
|
||||||
|
|
||||||
|
Three areas the operator named on 2026-10-04, as their own modules. Each sharpens a row of
|
||||||
|
[01](01-what-the-workstations-run.md) and a question of [02](02-the-questions-and-the-options.md).
|
||||||
|
|
||||||
|
## The screensaver: idle, lock and display power
|
||||||
|
|
||||||
|
**Measured on both workstations:**
|
||||||
|
|
||||||
|
- **Idle and lock** are three things wired by hand in the session's start script:
|
||||||
|
- the X screensaver timeout (`xset s 1800`);
|
||||||
|
- the display power timeouts (`xset dpms`);
|
||||||
|
- `xss-lock` running the colour build of `i3lock` through a wrapper, in a respawn loop.
|
||||||
|
- **A second screensaver,** xscreensaver, is installed and deliberately not started. Earlier it
|
||||||
|
overrode the display power settings with its own, and locked nothing. Its configuration file is
|
||||||
|
still in the home.
|
||||||
|
- **The lock screen's 20-odd colours and formats** were predecessor theme variables.
|
||||||
|
- **The colour build is not in the official repositories** (research 026/01).
|
||||||
|
|
||||||
|
**Starting position:**
|
||||||
|
|
||||||
|
- **One module for the lock screen,** holding `node-lock-screen`: the locker and its wrapper as the
|
||||||
|
module's own files, the screensaver and display power timeouts, and `xss-lock`.
|
||||||
|
- The timeouts and colours are its defaults, and settings later (issue 168).
|
||||||
|
- The colour build ships as ADR 0205's pinned archive, or the module uses the official `i3lock`.
|
||||||
|
That is the operator's choice, and the colours are the only difference.
|
||||||
|
- xscreensaver is not a module; its package and file are removed.
|
||||||
|
- `xss-lock` needs the logind session, so it stays a session-start line contributed into
|
||||||
|
`.xinitrc`'s block (question 4), not a unit.
|
||||||
|
|
||||||
|
## Monitor layout (xrandr)
|
||||||
|
|
||||||
|
**Measured:**
|
||||||
|
|
||||||
|
- Each workstation has a layout script generated by `arandr`, with the monitor names baked in. One
|
||||||
|
workstation also has several layouts for named places, a hotplug rule and a wizard.
|
||||||
|
- **The desktop carried the laptop's layout scripts.**
|
||||||
|
- No `xorg.conf.d`, and no layout tool beyond the scripts.
|
||||||
|
|
||||||
|
**Starting position: `autorandr`** (official repositories) inside the display server's module.
|
||||||
|
|
||||||
|
- `autorandr` saves a layout as a profile **keyed by the connected monitors' identities** (their EDID)
|
||||||
|
and applies the matching one at login and on hotplug.
|
||||||
|
- Profiles therefore need no machine's name. A profile can be shared mesh-wide and simply never
|
||||||
|
matches on a machine without those monitors. That is exactly the "say it by what is there, never by
|
||||||
|
a name" rule (ADR 0112).
|
||||||
|
- The profiles are the operator's data, saved by the tool itself, so they are *found* (ADR 0182). A
|
||||||
|
`layout` verb on `node-display-server` lists, saves and applies them.
|
||||||
|
- The arandr scripts and the hotplug rule retire once a profile exists for each.
|
||||||
|
|
||||||
|
## Menus: rofi and dmenu
|
||||||
|
|
||||||
|
**Measured:**
|
||||||
|
|
||||||
|
- rofi is the launcher, the power menu, the theme picker and the clipboard menu.
|
||||||
|
- The operator's scripts call `rofi -dmenu` in four places and **plain `dmenu` in two. dmenu is
|
||||||
|
installed on neither workstation, so those two fail.**
|
||||||
|
|
||||||
|
**Starting position:**
|
||||||
|
|
||||||
|
- **`rofi` holds `node-launcher`**, and the seat's protocol includes a **dmenu-compatible command**:
|
||||||
|
read choices on standard input, print the chosen one. Scripts call that command, not a program by
|
||||||
|
name.
|
||||||
|
- **`dmenu` is a module of its own** (official repositories), able to hold the same seat on a machine
|
||||||
|
that wants it, for instance a Wayland session where `wofi` or `fuzzel` would hold it instead.
|
||||||
|
- The rofi module carries its theme files, and the menus that belong to other modules arrive as those
|
||||||
|
modules' scripts:
|
||||||
|
- power menu → the session;
|
||||||
|
- clipboard menu → the clipboard module;
|
||||||
|
- theme picker → settings, once issue 168 closes.
|
||||||
|
|
||||||
|
## The clipboard: xclip and greenclip
|
||||||
|
|
||||||
|
**Measured:**
|
||||||
|
|
||||||
|
- **greenclip** keeps the clipboard's history, and rofi shows it on a key binding.
|
||||||
|
- **greenclip is not in the official repositories.**
|
||||||
|
- It is started two ways: the window manager's configuration starts it on both workstations, and on
|
||||||
|
one a user unit is enabled as well.
|
||||||
|
- Its configuration names an absolute home path.
|
||||||
|
- **xclip** (official) is the command-line clipboard the operator's scripts use.
|
||||||
|
|
||||||
|
**Starting position:**
|
||||||
|
|
||||||
|
- **`xclip` is a module of its own,** a package and nothing else. It is the tool scripts depend on,
|
||||||
|
and a module that needs it requires it.
|
||||||
|
- **The clipboard manager holds `node-clipboard`:** its daemon, started once by the session (a session
|
||||||
|
contribution, or a user unit once user-scoped units ship, never both), its configuration with no
|
||||||
|
absolute path, and its menu binding contributed to the window manager.
|
||||||
|
- **Which manager holds it is the operator's choice:**
|
||||||
|
- greenclip, as today, shipped under ADR 0205;
|
||||||
|
- or `clipmenu` (official), which feeds the same dmenu-compatible command as the launcher seat above,
|
||||||
|
and needs no archive.
|
||||||
|
- **On Wayland** the same seat is held by `cliphist` with `wl-clipboard`, both official.
|
||||||
|
|
||||||
|
## Fonts
|
||||||
|
|
||||||
|
**Measured:**
|
||||||
|
|
||||||
|
- The fonts the desktop uses are **hand-copied files** in the account's font directory, not packages:
|
||||||
|
- a Nerd font for the window manager, the bar and the terminal;
|
||||||
|
- a second one for the prompt;
|
||||||
|
- on one workstation, the same four files twice, once under URL-encoded names;
|
||||||
|
- on the other, a different build of the same font and three more copied from a theme's repository.
|
||||||
|
- The system's default monospace is a different font (`Noto Sans Mono`), so anything that asks for
|
||||||
|
`monospace` gets another face than the terminal.
|
||||||
|
- The DPI is fixed in an X resource.
|
||||||
|
- **Every Nerd font in use is in the official repositories** (Hack, Meslo, Iosevka, JetBrains Mono).
|
||||||
|
|
||||||
|
**Decided** (the operator left the choice open, except that it must not be today's Hack):
|
||||||
|
|
||||||
|
| role | face | why |
|
||||||
|
|---|---|---|
|
||||||
|
| monospace: terminal, window manager, bar, launcher, prompt | **JetBrains Mono Nerd Font** | built for long reading in a terminal, unambiguous `0O1lI`, optional ligatures; a version-3 Nerd font, so every icon the prompt and bar use is present |
|
||||||
|
| interface: GTK, Qt, notifications | **Inter** | designed for screens, clear at small sizes |
|
||||||
|
| icons missing from any face | Nerd Fonts Symbols | a fallback, so a font without icons still shows them |
|
||||||
|
| emoji | Noto Color Emoji | |
|
||||||
|
| serif and every other script | Noto | |
|
||||||
|
|
||||||
|
All five are official packages.
|
||||||
|
|
||||||
|
**Starting position:**
|
||||||
|
|
||||||
|
- **A `fonts` module:** those packages, and a fontconfig file it owns that maps `monospace`, `sans-serif`,
|
||||||
|
`serif` and the emoji and symbol fallbacks to the chosen faces, so every program agrees.
|
||||||
|
- The terminal, bar, launcher and prompt modules name the family, not a file.
|
||||||
|
- The DPI becomes the display server's setting (issue 168).
|
||||||
|
- The copied files are removed by the operator once the packages are in (ADR 0182).
|
||||||
|
- Fonts are not a seat: several coexist. The module owns the one place where *the* default is said.
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# 05 — The tools each module serves
|
||||||
|
|
||||||
|
A first catalogue for the modules of research 026 and 027, as the operator asked: "all kinds of useful
|
||||||
|
tools for all these modules". Each tool is served by the node's runtime (ADR 0175), on the machine the
|
||||||
|
module runs on. Through discovery (ADR 0195) it is reachable from any machine as
|
||||||
|
`<machine>/<module>.<tool>`, or as `<machine>/<seat>.<verb>` where a seat defines it.
|
||||||
|
|
||||||
|
**Conventions:**
|
||||||
|
|
||||||
|
- **(r)** reads.
|
||||||
|
- **(a)** acts on the machine, escalating where it must, as the packet filter does (to-be 38 WP4).
|
||||||
|
- **(d)** is a desktop act that needs the operator's session.
|
||||||
|
- A tool that changes something a module declares says so in its answer: the next push restores the
|
||||||
|
declaration.
|
||||||
|
- Every tool answers structured data, not prose (issue 229).
|
||||||
|
- **Seat verbs** (marked *seat*) are the protocol every holder of that seat serves. The rest are the
|
||||||
|
module's own.
|
||||||
|
|
||||||
|
## The graphical session (026)
|
||||||
|
|
||||||
|
| module | tools |
|
||||||
|
|---|---|
|
||||||
|
| `xorg` (*node-display-server*) | *seat* `displays` (r: outputs, modes, rates, connected monitors with their identity) · *seat* `layout` (r/a: list, save, apply an autorandr profile) · `set-mode` (a: one output's resolution, rate, rotation, scale) · `primary` (a) · `dpi` (r/a) · `input-devices` (r) · `input-set` (a: touchpad tap, natural scroll, pointer speed) · `keyboard` (r/a: layout and options) · `screenshot` (d: one screen or all, as a file) · `x-log` (r: the server's errors since start) |
|
||||||
|
| `i3` (*node-display-session*) | *seat* `reload` (a) · *seat* `workspaces` (r) · *seat* `windows` (r: tree with classes, titles, workspaces) · `focus` (d: window or workspace) · `move` (d: window to workspace or output) · `layout-save` / `layout-restore` (d: a workspace's arrangement) · `exec` (d: start a program in the session) · `kill` (d) · `bindings` (r: every key binding and what it runs) · `config-check` (r: validate the composed configuration before a reload) · `marks` (r) · `scratchpad` (d) |
|
||||||
|
| `sway` (*node-display-server*, *node-display-session*) | the same seat verbs over Wayland, plus `outputs` (r) and `idle-inhibitors` (r) |
|
||||||
|
| `lemurs` (*node-login-manager*) | *seat* `sessions` (r: what the login screen offers) · *seat* `default-session` (r/a) · `logins` (r: who logged in when, from the journal) |
|
||||||
|
| `xterm` (*node-terminal-emulator*) | *seat* `open` (d: a terminal, optionally running a command, in a directory) · `font` (r/a: face and size) · `colours` (r) |
|
||||||
|
| `i3status-rust` (*node-bar*) | *seat* `reload` (a) · `blocks` (r: what the bar shows and each block's current value) · `block-run` (r: run one block once and answer its output) · `themes` (r) |
|
||||||
|
| `picom` (*node-compositor*) | *seat* `restart` (a) · `rules` (r: opacity, shadow and blur rules in force) · `window-opacity` (d) · `toggle` (d: compositing off and on, for a game or a test) |
|
||||||
|
| `rofi` (*node-launcher*) | *seat* `menu` (d: show a list, answer the chosen line: the dmenu-compatible command as a tool) · `applications` (r: the desktop entries it would offer) · `themes` (r) · `run` (d) |
|
||||||
|
| `dmenu` (*node-launcher*) | *seat* `menu` (d) |
|
||||||
|
| `dunst` (*node-notifier*) | *seat* `send` (d: title, body, urgency, actions) · *seat* `history` (r) · `pause` / `resume` (d: do not disturb) · `close-all` (d) · `rules` (r) · `count` (r: shown, waiting, history) |
|
||||||
|
| lock module (*node-lock-screen*) | *seat* `lock` (d) · `idle` (r/a: screensaver and display power timeouts) · `inhibit` (d: keep the screen on for a while) · `locked` (r: is the session locked now, and since when) |
|
||||||
|
| clipboard manager (*node-clipboard*) | *seat* `history` (r: entries, newest first, length-limited) · *seat* `copy` (d: put text on the clipboard) · `paste` (r: what the clipboard holds now) · `clear` (d) · `delete` (d: one entry) |
|
||||||
|
| `xclip` | `copy` (d) · `paste` (r): the plain clipboard without a manager |
|
||||||
|
| `feh` (wallpaper) | `set` (d: an image, per output) · `current` (r) |
|
||||||
|
| `fonts` | `families` (r: installed faces) · `match` (r: what `monospace`, `sans-serif` and `emoji` resolve to) · `glyph` (r: which installed font has a given character) · `cache-rebuild` (a) |
|
||||||
|
| theme module | `appearance` (r/a: dark or light, for GTK, Qt and the portal at once) · `cursor` (r/a) · `icons` (r) · `portal-check` (r: which portal backend answers which interface) |
|
||||||
|
| `gnome-keyring` (*node-secret-service*) | *seat* `unlocked` (r) · `lock` (d) · `collections` (r: names and item counts, never secrets) · `ssh-keys` (r: what the agent holds, by fingerprint) |
|
||||||
|
| desktop hardware module (laptop) | `brightness` (r/a: panel and keyboard) · `battery` (r: charge, health, cycles, limit) · `charge-limit` (r/a) · `gpu-mode` (r/a: integrated, hybrid, discrete) · *seat* `profile` (r/a: quiet, balanced, performance) · `thermals` (r: temperatures and fan speeds) · `power-draw` (r) |
|
||||||
|
|
||||||
|
## The system and the account (027)
|
||||||
|
|
||||||
|
| module | tools |
|
||||||
|
|---|---|
|
||||||
|
| `docker` (*node-container-runtime*, ADR 0166) | *seat* `list`, `inspect`, `logs`, `stats`, `start`, `stop`, `restart` (r/a) · `images` (r: with size and which container uses each) · `prune` (a: dangling images, stopped containers not held by the mesh, build cache, with a dry run first) · `disk-usage` (r) · `networks` (r) · `volumes` (r: with what mounts each and whether the mesh holds it) · `events` (r: the last hour) · `daemon-config` (r) |
|
||||||
|
| `docker-compose` | `projects` (r: compose projects running and where their files are) · `up` / `down` / `restart` (a: one project, by directory) · `logs` (r) · `ps` (r) |
|
||||||
|
| `sudo` | `rules` (r: what the account may run, without a prompt and with one) · `check` (r: does the escalation the mesh relies on work here) |
|
||||||
|
| `pacman` | `search` (r) · `installed` (r: with version and explicitly or as a dependency) · `info` (r) · `owns` (r: which package owns a path) · `files` (r) · `updates` (r: what an upgrade would change) · `upgrade` (a: with the news first) · `orphans` (r) · `remove-orphans` (a) · `cache` (r/a: size, clean to the last N versions) · `history` (r: installs and upgrades from the log) · `mirrors` (r/a: rank and refresh) · `news` (r: distribution news since the last upgrade) |
|
||||||
|
| AUR (package repository, 027 question 1) | `search` (r) · `build` (a: on the build machine, into the mesh's repository) · `outdated` (r) · `published` (r) |
|
||||||
|
| `snapd`, `flatpak` | `list` (r) · `install` / `remove` (a) · `update` (a) · `runtimes` (r) · `disk-usage` (r) |
|
||||||
|
| `time-sync` | `status` (r: synchronised, offset, server) · `servers` (r) · `sync-now` (a) |
|
||||||
|
| `localization` | `get` (r: locale, time zone, keymap) · `time-zone` (r/a) · `locales` (r) |
|
||||||
|
| `kernel` | `running` (r: version, command line, uptime) · `installed` (r) · `modules` (r: loaded, with what uses them) · `reboot-needed` (r: a newer kernel or library than the one running) · `microcode` (r) · `boot-entries` (r) · `initramfs-rebuild` (a) · `dmesg` (r: errors since boot) |
|
||||||
|
| `logrotate` | `status` (r: last rotation per log) · `force` (a: one configuration) · `big-logs` (r: the largest logs on the machine) |
|
||||||
|
| `avahi` | `browse` (r: services on the local network) · `resolve` (r) |
|
||||||
|
| `cups` | `printers` (r) · `queue` (r) · `cancel` (a) · `print` (a: a file to a printer) · `default` (r/a) |
|
||||||
|
| `bluetooth` | `devices` (r: paired, connected, battery where reported) · `connect` / `disconnect` (a) · `scan` (r) · `power` (r/a) |
|
||||||
|
| `ssh-client` (owns `~/.ssh`) | `hosts` (r: every `Host` and where it came from: the mesh, a module, the operator) · `check` (r: modes, keys without a passphrase, keys unused for a year, stale `known_hosts` entries) · `authorized` (r: who may log in, by fingerprint and comment) · `revoke` (a: one authorized key, into the operator's region) · `known-host` (r/a: verify, refresh one host's key) · `test` (r: can this machine reach a host and authenticate, batch mode) |
|
||||||
|
| `sshd` | `sessions` (r: who is logged in, from where) · `config-effective` (r: `sshd -T`) · `failed-logins` (r: since a time, with fail2ban's verdicts) |
|
||||||
|
| scripts modules | `list` (r: each script with its one-line description) · `run` (a: one script by name with arguments, as the account, bounded like `execute`) · `which` (r: which module ships a command) |
|
||||||
|
| `node-env` (*node-environment*) | `show` (r: every variable and `PATH` entry with the module that contributed it) · `diff` (r: what a shell actually has versus what the mesh composed) |
|
||||||
|
| `zsh` (*node-login-shell*) | *seat* `execute` · `zsh_config` (r) · `history-search` (r: the account's history, by pattern) · `functions` (r: aliases and functions in force, with where each came from) · `startup-time` (r: how long an interactive shell takes to start, per slot) |
|
||||||
|
| `memory-pressure` | `status` (r: memory, swap, compressed swap ratio, pressure stall) · `top` (r: the largest processes) · `oom-history` (r: what was killed, when) |
|
||||||
|
| `zfs` | `pools` (r: health, capacity, fragmentation) · `datasets` (r) · `snapshots` (r/a: list, create, destroy by name) · `scrub` (r/a: status, start) · `errors` (r) · `arc` (r: cache statistics) |
|
||||||
|
| `nfs-server`, `samba` | `exports` / `shares` (r) · `clients` (r: who has it mounted now) · `reload` (a) |
|
||||||
|
| `nfs-client`, `smb-client` | `mounts` (r: each share, mounted or not, and since when) · `mount` / `unmount` (a) · `test` (r: is the server reachable, is the export offered) |
|
||||||
|
| hosts-file holder (*node-hosts-file*, ADR 0199) | *seat* `entries`, `add`, `remove` |
|
||||||
|
| `vnstat`, `lm_sensors` | `traffic` (r: per interface, day, month) · `sensors` (r) |
|
||||||
|
| mail consumer (future effort) | `accounts` (r) · `search` (r) · `unread` (r) · `read` (r: one message) · `mark` (a) · `send` (a) |
|
||||||
|
|
||||||
|
## What this catalogue is for
|
||||||
|
|
||||||
|
It is a starting list, not a contract. A tool becomes a contract only when it is a seat's verb, and
|
||||||
|
each seat's verbs are decided in that seat's record (ADR 0132). A module's own tools can grow freely.
|
||||||
|
Every row above is a tool the operator would otherwise run by hand over ssh. That is the measure of
|
||||||
|
whether one is worth writing.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
status: active
|
||||||
|
initiated: 2026-10-04
|
||||||
|
touches:
|
||||||
|
- 02-DECISIONS/0165-container-runtime-is-what-a-machine-can-run-and-a-running-runtime-is-its-holders-health.md
|
||||||
|
- 02-DECISIONS/0166-the-container-runtime-is-a-node-seat-and-the-host-creates-containers-through-its-holder.md
|
||||||
|
- 02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md
|
||||||
|
- 02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md
|
||||||
|
- 03-DESIGN/01-to-be/37-the-operators-machine.md
|
||||||
|
- 03-DESIGN/01-to-be/38-building-the-operators-machine.md
|
||||||
|
became: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# 027 — The system layer as modules
|
||||||
|
|
||||||
|
## What is investigated
|
||||||
|
|
||||||
|
What runs on the machines below the operator's home and outside the mesh's own services, and which
|
||||||
|
of it should be modules. That covers:
|
||||||
|
|
||||||
|
- the container runtime and its tools;
|
||||||
|
- privilege (sudo);
|
||||||
|
- the package manager and the software it cannot install;
|
||||||
|
- time, locale, the kernel and boot;
|
||||||
|
- log rotation;
|
||||||
|
- the machine-specific daemons the workstations and servers carry: printing, bluetooth, VPN
|
||||||
|
clients, virtualisation, storage, sharing.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The operator asked for the system level beside the graphical session. In particular:
|
||||||
|
|
||||||
|
- a `docker` module (decided in principle by the proposed ADRs 0165 and 0166, never built);
|
||||||
|
- a `docker-compose` module for development work, assigned **only to the two workstations**.
|
||||||
|
|
||||||
|
Measured in [01](01-what-the-machines-run.md): on four machines, almost nothing at this level is
|
||||||
|
owned by a module. The pieces differ by machine for no recorded reason. Three findings are security
|
||||||
|
matters on their own.
|
||||||
|
|
||||||
|
## How it is approached
|
||||||
|
|
||||||
|
**Adopting is also improving** (the operator, 2026-10-04). A module is not a copy of what a machine
|
||||||
|
does today. Making it is the moment to fix what is broken, drop what is dead, choose the better tool
|
||||||
|
and remove the leftovers. Every module's design lists its improvements over today. **Every module
|
||||||
|
also serves tools,** many of them, for reading, acting and diagnosing; a module that only places a
|
||||||
|
package and a file is unfinished. The tools are catalogued in
|
||||||
|
[026/05](../026-the-graphical-session-as-modules/05-the-tools-each-module-serves.md).
|
||||||
|
|
||||||
|
## What it touches
|
||||||
|
|
||||||
|
- **The container runtime seat** (ADRs 0165 and 0166, both proposed).
|
||||||
|
- **The host's `package` shape**, which installs from the distribution's official repositories only,
|
||||||
|
while the workstations carry 67 and 114 packages from elsewhere.
|
||||||
|
- **How a secret reaches the account's environment.** ADR 0203 forbids it in the contributed
|
||||||
|
environment, but a predecessor file supplies such secrets today.
|
||||||
|
- **The facts the mesh assumes and never declares,** above all that the operator account escalates
|
||||||
|
without a prompt.
|
||||||
|
|
||||||
|
## Documents
|
||||||
|
|
||||||
|
- [01 — What the machines run](01-what-the-machines-run.md): evidence.
|
||||||
|
- [02 — Candidates and questions](02-candidates-and-questions.md)
|
||||||
|
- [03 — The account's own tools](03-the-accounts-own-tools.md): `~/.ssh` as one module's, scripts on every machine, the keyring, the laptop's power management, mail as events
|
||||||
|
- The predecessor's lessons, shared with research 026: [026/03](../026-the-graphical-session-as-modules/03-what-the-predecessor-taught.md)
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# 01 — What the machines run
|
||||||
|
|
||||||
|
Measured 2026-10-04 on four machines, read-only, including the host's own record of what it applied:
|
||||||
|
two servers (the anchor and a home server) and two workstations (a laptop and a desktop). "Owned"
|
||||||
|
means a module the mesh assigns declares it.
|
||||||
|
|
||||||
|
## The container runtime
|
||||||
|
|
||||||
|
| | anchor | home server | laptop | desktop |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| docker | 29.8.2 | 29.8.2 | 29.7.2 | 29.7.2 |
|
||||||
|
| compose | 5.5.1 | 5.6.0 | 5.5.0 | 5.5.0 |
|
||||||
|
| buildx | 0.37.2 | — | — | — |
|
||||||
|
| podman | — | 6.1.3 | 6.1.0 | 6.1.0 |
|
||||||
|
| `docker.socket` | disabled | enabled | enabled | enabled |
|
||||||
|
| `containerd.service` | disabled | disabled | disabled | **enabled** |
|
||||||
|
| `daemon.json` beyond the shared keys | direct routing, two more insecure registries | log rotation (100 MB × 10) | — | — |
|
||||||
|
| docker group | operator, **a CI user** | operator | operator | operator |
|
||||||
|
|
||||||
|
**Ownership:**
|
||||||
|
|
||||||
|
- The `docker` package is owned on one machine only, by the installer's bootstrap, not by a module.
|
||||||
|
- `docker.service` is declared indirectly, by the name resolver and the private-network modules,
|
||||||
|
which each merge their own keys into `daemon.json`.
|
||||||
|
- Nothing owns the socket, containerd, compose, buildx or the group.
|
||||||
|
|
||||||
|
**Compose in use:**
|
||||||
|
|
||||||
|
- On the servers, no running container belongs to a compose project. Their compose files are
|
||||||
|
pre-mesh trees under the operator's and root's homes, plus a dangling enabled unit for one of them.
|
||||||
|
- On the workstations, compose runs development stacks, and pre-mesh service trees sit under a
|
||||||
|
top-level directory.
|
||||||
|
|
||||||
|
The mesh marks its own containers with a host label. On the workstations, a handful of unlabelled
|
||||||
|
development and test containers run beside its build agent.
|
||||||
|
|
||||||
|
## Privilege
|
||||||
|
|
||||||
|
- The operator account escalates **without a prompt on all four machines**. The mesh relies on this,
|
||||||
|
but it is set by hand in `/etc/sudoers` (a `wheel` rule on two machines, the account named on
|
||||||
|
two), and nothing declares it.
|
||||||
|
- On the anchor, a **CI user from the predecessor** keeps passwordless sudo and docker membership,
|
||||||
|
and a predecessor drop-in in `sudoers.d` survives.
|
||||||
|
- On the desktop, the operator account is also in the **`root` group**.
|
||||||
|
|
||||||
|
## The package manager
|
||||||
|
|
||||||
|
- `pacman.conf` is stock except on one server (parallel downloads).
|
||||||
|
- The mirror list was generated once by a tool that is no longer installed. On the anchor, it is the
|
||||||
|
hosting provider's single mirror.
|
||||||
|
- An AUR helper is installed everywhere.
|
||||||
|
- **Packages from outside the official repositories:** 2 on the anchor, 21 on the home server,
|
||||||
|
67 on the laptop, 114 on the desktop. They include:
|
||||||
|
- the agent CLI, which a catalogue module declares as a package and the host cannot install;
|
||||||
|
- a VPN client;
|
||||||
|
- a remote-access client;
|
||||||
|
- printer drivers;
|
||||||
|
- GPU tools;
|
||||||
|
- a kernel module built from source (DKMS) for a storage filesystem;
|
||||||
|
- a snap daemon.
|
||||||
|
|
||||||
|
## Time, locale, kernel, boot
|
||||||
|
|
||||||
|
| | anchor | home server | laptop | desktop |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| time zone, keymap | **another zone**, a non-US console keymap | local zone, unset | local zone, unset | local zone, unset |
|
||||||
|
| time sync | timesyncd plus a provider drop-in | timesyncd | timesyncd | **ntpd**, timesyncd disabled |
|
||||||
|
| bootloader | grub (BIOS) | systemd-boot **and** grub | systemd-boot | systemd-boot **and** grub |
|
||||||
|
| kernels | one | two, plus a DKMS filesystem module | one | one, plus a DKMS controller driver |
|
||||||
|
| microcode | **none** | yes | yes | **none** |
|
||||||
|
| swap | RAID partition | partition | zram, a file and a partition | partition |
|
||||||
|
| log rotation timer | not found | enabled | not found | not found |
|
||||||
|
|
||||||
|
## Daemons and services no module owns
|
||||||
|
|
||||||
|
- **All four:** avahi.
|
||||||
|
- **Workstations:**
|
||||||
|
- a VPN client daemon (both);
|
||||||
|
- virtualisation (incus) with a hand-made unit that inserts container-runtime firewall rules (both);
|
||||||
|
- printing and bluetooth;
|
||||||
|
- GPU and power tuning per model;
|
||||||
|
- a remote-access daemon (laptop);
|
||||||
|
- snap and flatpak (desktop);
|
||||||
|
- the local model server, run from a hand-written unit although a catalogue module for it exists
|
||||||
|
(desktop);
|
||||||
|
- Samba sharing and a network filesystem mount from the home server (desktop). A second mount is
|
||||||
|
failing, and its **credential is written in clear in `/etc/fstab`**.
|
||||||
|
- **Servers:**
|
||||||
|
- a storage pool (about 167 TB) with its import, mount and scrub units, an NFS server and Samba
|
||||||
|
sharing (home server);
|
||||||
|
- traffic and sensor monitoring (home server);
|
||||||
|
- a DHCP client daemon the catalogue has a module for but does not assign there (home server);
|
||||||
|
- cron, an entropy daemon, and the **legacy `iptables` services**, which run beside the mesh's own
|
||||||
|
filter (anchor).
|
||||||
|
- **Not found anywhere:** a backup agent, a monitoring agent, a second VPN mesh.
|
||||||
|
|
||||||
|
## What is plain debris
|
||||||
|
|
||||||
|
- Dangling enabled-unit links on three machines.
|
||||||
|
- Predecessor blocks in `/etc/hosts` on both servers.
|
||||||
|
- The CI user, and the predecessor sudoers drop-in, on the anchor.
|
||||||
|
- Pre-mesh compose trees on the anchor, the home server and the desktop.
|
||||||
|
- Unlabelled test containers on the workstations.
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# 02 — Candidates and questions
|
||||||
|
|
||||||
|
## Decided by the operator on 2026-10-04
|
||||||
|
|
||||||
|
- **`docker`** holds the container runtime seat on every machine, as ADRs 0165 and 0166 propose. Those
|
||||||
|
records are promoted from proposed when it is built.
|
||||||
|
- **`docker-compose` is a module of its own,** the distribution's package and nothing else. It is
|
||||||
|
assigned **only to the two workstations**, for development work. The servers run nothing through
|
||||||
|
compose.
|
||||||
|
|
||||||
|
Later the same day, on the candidates below:
|
||||||
|
|
||||||
|
- **Yes, all of them:** `docker`, `docker-compose`, `sudo`, `pacman`, an AUR helper (question 1),
|
||||||
|
`time-sync`, `kernel` (with boot and microcode), `logrotate`, `avahi`, `cups` with the printer's
|
||||||
|
driver, and every server-only candidate.
|
||||||
|
- **Locale, time zone and keymap are one module, `localization`.**
|
||||||
|
- **`snapd` and `flatpak`** are modules, on the two workstations only.
|
||||||
|
- **`incus` is the lab's,** whose module depends on it. It is not a module of its own beside the lab.
|
||||||
|
- **The agent's and the local model server's modules are still being developed,** and are not
|
||||||
|
assigned until they are.
|
||||||
|
- **The predecessor's CI user is retired.** It was removed from the anchor the same day, with its
|
||||||
|
sudoers line, its docker membership and a dangling unit link; the backup is on the machine.
|
||||||
|
|
||||||
|
## Candidate modules
|
||||||
|
|
||||||
|
**On every machine:**
|
||||||
|
|
||||||
|
| module | owns | first reason |
|
||||||
|
|---|---|---|
|
||||||
|
| `docker` | the packages (runtime, containerd), the service and socket, `daemon.json`'s base keys (live restore, log rotation), the docker group's members | four machines, four configurations, one owner on one |
|
||||||
|
| `sudo` | the operator account's escalation as a drop-in, declared | the mesh's tools rely on it (to-be 38 WP4) and nothing states it |
|
||||||
|
| `pacman` | `pacman.conf`'s few keys, the mirror list and its refresher, cache cleaning | mirrors generated once and never again |
|
||||||
|
| `time-sync` | timesyncd and its drop-ins | two daemons across four machines |
|
||||||
|
| `localization` | locale, time zone, console keymap (one module, the operator's choice) | one machine differs, with no record why |
|
||||||
|
| `kernel` | the kernel packages, microcode, initramfs presets | two machines without microcode |
|
||||||
|
| `logrotate` | the timer and the base configuration | rotation runs on one machine of four |
|
||||||
|
| `avahi` | the daemon and name-service switch entry | on all four, owned by none |
|
||||||
|
|
||||||
|
**On the workstations only:**
|
||||||
|
|
||||||
|
- `docker-compose`;
|
||||||
|
- `lemurs`, the login manager (research 026);
|
||||||
|
- a VPN client module;
|
||||||
|
- `incus` with its forward unit (the lab module declares the package on one workstation only);
|
||||||
|
- `cups` with the printer's driver;
|
||||||
|
- `bluetooth`;
|
||||||
|
- per-model **hardware** modules: GPU, power, vendor keys, brightness. These are the same modules
|
||||||
|
research 026 needs for the desktop's fragments.
|
||||||
|
|
||||||
|
**On the servers only:**
|
||||||
|
|
||||||
|
- `zfs` with its scrub timer, and the long-term kernel it builds against;
|
||||||
|
- `nfs-server`;
|
||||||
|
- `samba`;
|
||||||
|
- `vnstat`, `lm_sensors`.
|
||||||
|
|
||||||
|
`cron` on the anchor serves one stock file and can go. So can the entropy daemon on a modern
|
||||||
|
kernel.
|
||||||
|
|
||||||
|
**Retire, not model:** the legacy `iptables` services on the anchor. They duplicate the mesh's
|
||||||
|
filter, which is ADR 0100's ground.
|
||||||
|
|
||||||
|
## Questions this effort has to answer
|
||||||
|
|
||||||
|
1. **Software outside the official repositories.** The host's `package` shape installs from the
|
||||||
|
official repositories only. A catalogue module already declares an AUR package (the agent CLI),
|
||||||
|
which no machine could install, and the workstations carry 181 such packages between them.
|
||||||
|
|
||||||
|
| | option | for | against |
|
||||||
|
|---|---|---|---|
|
||||||
|
| P1 | ADR 0205's pinned vendored archive, per piece | exists | wrong for packages that build native code or kernel modules |
|
||||||
|
| P2 | **The build machine builds AUR packages into a package repository the mesh serves** from its artifact store. The host then installs them as packages, signed | one shape for every package; pinned, reviewed and built once | a repository to serve and a signing key to keep |
|
||||||
|
| P3 | An AUR helper on each machine, driven by the host | nothing to serve | builds on every machine, unpinned: the predecessor's `git clone` in another form |
|
||||||
|
|
||||||
|
Starting position: P2, for anything with native code. ADR 0205 stays for plain files such as a
|
||||||
|
theme.
|
||||||
|
|
||||||
|
2. **Secrets in the account's environment.** A predecessor file feeds package-registry and API tokens
|
||||||
|
to the session. ADR 0203 refuses secrets in contributed values, because they travel in the clear.
|
||||||
|
The candidate is ADR 0182's third class: a module's own process writes a mode-0600 file of
|
||||||
|
exports, from secrets the vault hands it over the bus, and the shell and the session source it.
|
||||||
|
This needs its own record.
|
||||||
|
|
||||||
|
3. **Per-machine sizing and drivers.** The swap layout, the GPU, the storage pool and the boot
|
||||||
|
loader are facts of one machine's hardware. They belong in hardware modules, or in settings
|
||||||
|
(issue 168), not in the shared ones.
|
||||||
|
|
||||||
|
4. **What a module may leave behind.** Compose is installed on both servers, unowned. The mesh
|
||||||
|
removes nothing it did not make. The choice is between an operator's one-off removal and a
|
||||||
|
server-side `absent` declaration.
|
||||||
|
|
||||||
|
5. **The hosts file.** ADR 0199 (decided on an open change, not yet merged)
|
||||||
|
gives `/etc/hosts` to one module through a seat, `node-hosts-file`, with an operator region and
|
||||||
|
three verbs. It is not built. Today the private network's foundation writes only its own block, and
|
||||||
|
the rest of each file is a predecessor's stale blocks (both servers) or the operator's development
|
||||||
|
names (both workstations). The candidate module is that seat's first holder. It takes the
|
||||||
|
private-network block as a contribution, and its operator region replaces the hand-kept lines.
|
||||||
|
|
||||||
|
6. **Mounts.** The host has no shape for a filesystem mount; ADR 0091 is about what a container
|
||||||
|
mounts. One workstation mounts a share of the home server over NFS, and a second share over SMB.
|
||||||
|
That second one fails, and its credential sits in clear in `/etc/fstab`.
|
||||||
|
|
||||||
|
| | option | for | against |
|
||||||
|
|---|---|---|---|
|
||||||
|
| M1 | **A module owns `/etc/fstab`** and other modules contribute lines | one file, as people know it | the file also carries the root and boot filesystems the installer wrote, which no module should rewrite; a slot contribution into a file that can stop a machine booting |
|
||||||
|
| M2 | **Each client module writes its own systemd mount (and automount) unit**, which is the service manager's drop-in for exactly this. The `nfs-client` or `smb-client` module declares the unit file and the service shape enables it. `/etc/fstab` stays the machine's | no new host shape, and no shared file; the unit names its own dependencies (network online, the private network) and an automount does not hang a boot when the server is away; unassigning removes the mount | a mount reads as a unit, not a line |
|
||||||
|
| M3 | A new `mount` shape in the host | the host knows what a mount is | a second way to say what M2 says |
|
||||||
|
|
||||||
|
Starting position: **M2.** The credential an SMB mount needs is a secret, written by the module's
|
||||||
|
own process from the vault, mode 0600, which is question 2's mechanism. The pair is a server
|
||||||
|
module exporting (`nfs-server`, `samba`) and a client module mounting. The client requires the
|
||||||
|
share the server provides, so the mount is resolved, not hand-typed.
|
||||||
|
|
||||||
|
7. **Two DHCP clients on one interface.** The home server runs `dhcpcd`, a DHCP *client* (no machine
|
||||||
|
runs a DHCP server), next to the network manager, which is its assigned networking module. Both
|
||||||
|
lease an address on the same interface, which therefore carries two LAN addresses. The catalogue's
|
||||||
|
`dhcpcd` module is assigned nowhere, and this unit is a leftover. The network manager is the
|
||||||
|
machine's one DHCP client, and `dhcpcd` should be disabled there.
|
||||||
|
|
||||||
|
## Security findings, independent of any module
|
||||||
|
|
||||||
|
1. A filesystem credential in clear text in a workstation's `/etc/fstab`, for a mount that is failing
|
||||||
|
anyway.
|
||||||
|
2. A predecessor CI user with passwordless sudo and docker membership on the anchor, and a
|
||||||
|
predecessor sudoers drop-in. *The user was removed on 2026-10-04; the drop-in remains.*
|
||||||
|
3. The operator account in the `root` group on one workstation.
|
||||||
|
|
||||||
|
Each is one small change. None waits for a module.
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# 03 — The account's own tools: ssh, scripts, mail
|
||||||
|
|
||||||
|
Three further directions from the operator on 2026-10-04. Each is account-level, like the shell
|
||||||
|
([to-be 41](../../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md)).
|
||||||
|
|
||||||
|
## `~/.ssh` is one module's
|
||||||
|
|
||||||
|
*"A module owns `~/.ssh`, so it is its responsibility that every folder is set up consistently and
|
||||||
|
correctly."*
|
||||||
|
|
||||||
|
**Measured:**
|
||||||
|
|
||||||
|
- The catalogue's `ssh-client` module owns the directory (mode 0700) and one region of
|
||||||
|
`~/.ssh/config`: a `Host` block per machine of the mesh. It owns nothing else.
|
||||||
|
- On one workstation, a predecessor's header, `Include` and hand-written host block sat **above** the
|
||||||
|
mesh's region. ssh takes the first match, so the predecessor's entries were the ones in force, for
|
||||||
|
the same machines. Removed on 2026-10-04.
|
||||||
|
- On the control machine, two keys of a retired CI system were still in the operator's
|
||||||
|
`authorized_keys`, able to log in as the operator. Removed the same day.
|
||||||
|
- Permissions differ by file and by machine. Backups of the configuration lie beside it.
|
||||||
|
|
||||||
|
**Starting position:** `ssh-client` becomes the holder of everything under `~/.ssh`, classified as
|
||||||
|
ADR 0182 asks:
|
||||||
|
|
||||||
|
| path | class | how |
|
||||||
|
|---|---|---|
|
||||||
|
| `~/.ssh/`, its mode, every file's mode | owned | the directory resource, plus a check verb that reports a file with the wrong mode |
|
||||||
|
| `~/.ssh/config` | written into, the mesh's block **at the start** | the mesh's hosts win; the operator's lines after it are kept; an `Include config.d/*` line in the block |
|
||||||
|
| `~/.ssh/config.d/<module>` | owned by the contributing module | ssh's own drop-in: a work module adds its forge's host there (research 026 C1) |
|
||||||
|
| `~/.ssh/authorized_keys` | written into, the mesh's block | the operator's keys as the mesh records them, and nothing a retired system left. The operator's own lines are kept below the block |
|
||||||
|
| `~/.ssh/known_hosts` | written into, the mesh's block | every mesh machine's host key, so the first connection never asks |
|
||||||
|
| private keys | found | never read and never written by the mesh; a key the mesh should hand out comes from the vault, through the module's own process (ADR 0182, third class) |
|
||||||
|
|
||||||
|
The sshd module is the other half: the machine's side. It is already in the catalogue.
|
||||||
|
|
||||||
|
## Scripts on every machine, shared and machine-specific
|
||||||
|
|
||||||
|
*"All nodes should get some custom scripts, both node-specific and mesh-specific (shared)."*
|
||||||
|
|
||||||
|
**Measured:** the operator's script folder holds 64 entries plus 33 in its `bin/`. It is under no
|
||||||
|
version control, and exists only where it was copied. It mixes three kinds:
|
||||||
|
|
||||||
|
1. scripts belonging to a module (the desktop's watchers, lock, menus; a laptop model's brightness);
|
||||||
|
2. the operator's own tools;
|
||||||
|
3. installers that modules have replaced.
|
||||||
|
|
||||||
|
**Starting position:**
|
||||||
|
|
||||||
|
- **The operator's scripts live in a repository of their own,** registered as any application is
|
||||||
|
([ADR 0015](../../02-DECISIONS/0015-applications-live-in-their-own-repository.md)), built as archives,
|
||||||
|
unpacked into a directory the module owns under the home. `bin/` goes on `PATH` through an
|
||||||
|
environment contribution ([ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md)),
|
||||||
|
and small functions go into the shell through a `shell` contribution
|
||||||
|
([ADR 0204](../../02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md)).
|
||||||
|
- **"Machine-specific" is said by assignment, never by naming a machine**
|
||||||
|
([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md)). One repository
|
||||||
|
holds several modules:
|
||||||
|
- `scripts` (shared, on every machine);
|
||||||
|
- `scripts-workstation`;
|
||||||
|
- `scripts-media`;
|
||||||
|
- and so on, each assigned where it applies.
|
||||||
|
|
||||||
|
A script that belongs to a piece of software or hardware moves into that module instead. A flavor
|
||||||
|
inside one module is what [research 026/03](../026-the-graphical-session-as-modules/03-what-the-predecessor-taught.md)
|
||||||
|
says not to repeat.
|
||||||
|
- **A script can also be a tool.** A script with a one-line description is served by the node's
|
||||||
|
runtime, so it can be called through the mesh on any machine that has it.
|
||||||
|
- A script that needs a secret gets it through question 2's mechanism, never from a file of
|
||||||
|
environment secrets.
|
||||||
|
|
||||||
|
## The keyring
|
||||||
|
|
||||||
|
*"A keyring is also a good thing to create a module for."*
|
||||||
|
|
||||||
|
**Measured on the two workstations, which both run GNOME Keyring:**
|
||||||
|
|
||||||
|
- **On one, the keyring unlocks at login.** The login manager's PAM service includes `login`, which
|
||||||
|
carries `pam_gnome_keyring`.
|
||||||
|
- **On the other, it does not.** The PAM line is only in the screensaver's service, so at session
|
||||||
|
start the window manager runs a script that asks for the password a second time and unlocks the
|
||||||
|
keyring with it.
|
||||||
|
- **On both, the session's start script starts the daemon again** with the ssh and gpg components,
|
||||||
|
and exports the ssh agent's socket. The keyring's current release serves the ssh agent through a
|
||||||
|
separate per-user socket unit instead.
|
||||||
|
|
||||||
|
**Starting position:** a `gnome-keyring` module that holds a node seat, `node-secret-service` (the
|
||||||
|
holder of the desktop's secret service; a password manager could hold it instead). It declares:
|
||||||
|
|
||||||
|
- the package;
|
||||||
|
- its lines in the login manager's PAM file, written into, never over (ADR 0102), so login unlocks it
|
||||||
|
on every machine;
|
||||||
|
- the ssh agent's user socket, once user-scoped units ship;
|
||||||
|
- the agent's socket path as an environment contribution, which needs a machine fact for the
|
||||||
|
account's runtime directory. ADR 0203 forbids `$` in values, so `$XDG_RUNTIME_DIR` cannot be
|
||||||
|
written in one.
|
||||||
|
|
||||||
|
The second unlock prompt and the second daemon start go away.
|
||||||
|
|
||||||
|
## Mail as events
|
||||||
|
|
||||||
|
*"Ideally a mail consumer with all my mail accounts configured, so my mail is recorded in the bus."*
|
||||||
|
|
||||||
|
**Measured:**
|
||||||
|
|
||||||
|
- The predecessor polled one work mailbox every minute. It **read an access token out of the mail
|
||||||
|
client's process memory**, called a mail API with it, and raised a desktop notification per unread
|
||||||
|
message. It worked only while the mail client ran, and stopped silently when the predecessor's units
|
||||||
|
were retired.
|
||||||
|
- Two further predecessor modules served mail tools, for one provider and for IMAP.
|
||||||
|
- The mesh runs a mail server of its own for its domains.
|
||||||
|
|
||||||
|
**Not decided here; it needs an effort of its own.** The questions it would have to answer:
|
||||||
|
|
||||||
|
- **Accounts and how each authenticates:**
|
||||||
|
- IMAP with an app password;
|
||||||
|
- a provider's OAuth with a registered application;
|
||||||
|
- the mesh's own mail server, which can publish delivery itself.
|
||||||
|
|
||||||
|
An employer's tenant may forbid registering an application at all.
|
||||||
|
- **What the bus records:**
|
||||||
|
- headers and a summary as events;
|
||||||
|
- bodies and attachments in an object store the event points at;
|
||||||
|
- retention, since mail is the most personal data the mesh would hold.
|
||||||
|
- **What consumes it:** a notifier bridge to the desktop (the predecessor's notifications), search,
|
||||||
|
an agent's context.
|
||||||
|
- **Where it runs:** one long-running module, not per machine (ADR 0198).
|
||||||
|
|
||||||
|
The obvious first step is the mail server the mesh already runs.
|
||||||
|
|
||||||
|
## Power management on the laptop
|
||||||
|
|
||||||
|
*"Power management for the laptop."*
|
||||||
|
|
||||||
|
**Measured on the laptop** (a gaming model with a hybrid GPU):
|
||||||
|
|
||||||
|
- **The platform profile is driven by a vendor daemon** (`asusd`) and its CLI. The vendor CLI is
|
||||||
|
now in the official repositories; the copy installed came from elsewhere. A predecessor script
|
||||||
|
runs as a user unit and switches the profile every five seconds: quiet on battery, balanced on
|
||||||
|
mains, performance above 50 % CPU.
|
||||||
|
- **The hybrid GPU's mode** (now hybrid) is held by a second vendor daemon (`supergfxd`), which is
|
||||||
|
**not** in the official repositories. Kernel-module options for the discrete GPU's power state
|
||||||
|
and its suspend, hibernate and resume units are set by hand. Its own power daemon is masked.
|
||||||
|
- **The battery charge limit is 80 %,** set by the vendor daemon.
|
||||||
|
- **The lid and power key suspend.** The brightness key is ignored by logind and handled by the
|
||||||
|
vendor-key path. Both are logind drop-ins.
|
||||||
|
- **Memory pressure:** compressed swap in RAM (`zram`) beside a swap file and a partition;
|
||||||
|
`systemd-oomd` with drop-ins; a predecessor *memory guard* user unit that notifies before the OOM
|
||||||
|
killer acts.
|
||||||
|
- `upower` runs. There is no `power-profiles-daemon`, `tlp`, `auto-cpufreq` or `thermald`, so nothing
|
||||||
|
competes with the vendor daemon, by design.
|
||||||
|
|
||||||
|
All of it came from two predecessor modules, one of which was a laptop-model *flavor*. A desktop
|
||||||
|
received part of it (research 026/01).
|
||||||
|
|
||||||
|
**Starting position:**
|
||||||
|
|
||||||
|
- **A hardware module per machine model** (here, the laptop's model). It holds the vendor daemon and
|
||||||
|
its profile configuration, the GPU mode daemon (ADR 0205's case, or the build machine's package
|
||||||
|
repository of research 027 question 1), the discrete GPU's module options and suspend units, the
|
||||||
|
logind drop-ins, the battery charge limit, and the vendor keys and brightness. It is assigned to
|
||||||
|
the one machine of that model, and to any second one later.
|
||||||
|
- **The profile switching** moves from a polling script to the module's own long-running code
|
||||||
|
(ADR 0198). It reacts to the power-supply change event instead of polling, and its thresholds
|
||||||
|
become settings (issue 168).
|
||||||
|
- **Memory pressure is not the laptop's alone.** `zram` and `systemd-oomd` with the notifier are a
|
||||||
|
`memory-pressure` module, assigned wherever wanted. The swap layout stays the machine's (`kernel`
|
||||||
|
module, question 3).
|
||||||
|
- A **`node-power-profile`** seat (vendor daemon, or `power-profiles-daemon` on other hardware)
|
||||||
|
gives the mesh one verb, `profile`, the same on every machine that has one.
|
||||||
+2
@@ -9,6 +9,8 @@ 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
|
# 174. A node varies a module through settings and kept regions, never through an edit
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-04, by [ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md).** Where this record calls a kept region *a marked block in which the operator's own lines are kept*, read the inverse, which is what the host built: the mesh's region is the marked block, and every line outside it is the operator's, kept byte for byte and given back when the module goes. The decision stands: a node varies a module by settings and by the operator's own lines, never by an edit.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
[ADR 0011](0011-managed-files-are-generated-never-edited.md) says a managed file is derived and an
|
[ADR 0011](0011-managed-files-are-generated-never-edited.md) says a managed file is derived and an
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ 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
|
# 176. The login shell is a node seat held by one shell module, and `execute` is its contract
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-04, by [ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md).** The seat is no longer declared by the shell modules (§1). It is `node-login-shell`, in the mesh's own seat set, which a shell module claims. Its holder also places the shell code other modules contribute, and sources the account's environment ([ADR 0203](0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md)). What stands: one holder per node, the login shell set by the `user` shape and given back, `execute` as the contract, and any node may call it.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
[ADR 0040](0040-what-a-module-is.md) names the shell as its example of a *shared* seat: bash, zsh
|
[ADR 0040](0040-what-a-module-is.md) names the shell as its example of a *shared* seat: bash, zsh
|
||||||
|
|||||||
+9
-3
@@ -9,6 +9,12 @@ 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
|
# 181. The operator account is a node fact, and a home is a placement root
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-04.** This record called a resource under a home *home-scoped*, and a
|
||||||
|
> module that places one a *home-scoped module*. There is no such kind of module
|
||||||
|
> ([ADR 0173](0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md) §2: a module is
|
||||||
|
> what it declares), so the three places now say *a resource placed under a home* and *a module placing
|
||||||
|
> files under a home*. What was decided is unchanged.
|
||||||
|
|
||||||
*Reconstructed. The controller shipped this on 2026-09-27 and
|
*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
|
[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
|
decision behind it. This record states what was decided, from the code and the design, and adds the
|
||||||
@@ -35,7 +41,7 @@ its home; the account and its home are machine facts a definition may name in a
|
|||||||
and content; a roster file may say it lives under the home, and is then rendered per node, placed under
|
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
|
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
|
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.
|
stated it, so no resource placed under a home can land anywhere yet.
|
||||||
|
|
||||||
## Considered Options
|
## Considered Options
|
||||||
|
|
||||||
@@ -68,7 +74,7 @@ account. A definition names the account and its home as machine facts, never as
|
|||||||
may say it is a home file and is then placed and owned the same way. The controller resolves both at
|
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.
|
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
|
**A node with no account cannot carry a resource placed under a home, 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
|
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.
|
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
|
A module that writes a person's files is thereby unassignable to a machine with no person on it, which
|
||||||
@@ -80,7 +86,7 @@ anything.
|
|||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- **The operator states the account before any home-scoped module lands.** Today none is stated, so the
|
- **The operator states the account before any module placing files under a home lands.** Today none is stated, so the
|
||||||
first assignment of such a module begins with four node records.
|
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
|
- 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.
|
on every machine — the gap that surfaced this, closed by the same fact.
|
||||||
|
|||||||
+9
-3
@@ -9,6 +9,12 @@ extends: 02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-
|
|||||||
|
|
||||||
# 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
|
# 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
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-04.** This record said *a home-scoped module* and *the family of
|
||||||
|
> home-scoped modules*. There is no such kind of module
|
||||||
|
> ([ADR 0173](0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md) §2), and the rule
|
||||||
|
> is about a directory under a home, whichever module declares it; the three places now say so. The
|
||||||
|
> decision, its options and its consequences are unchanged.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
[ADR 0181](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md) lets a module
|
[ADR 0181](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md) lets a module
|
||||||
@@ -35,7 +41,7 @@ use tools that no longer exist. Nothing owns them; nothing will ever rewrite or
|
|||||||
`~/.ssh`: the mesh owns the directory and the files it places; it holds the person's private keys and
|
`~/.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
|
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
|
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
|
to hold for every directory under a home that any module will touch, so it is a rule, not a
|
||||||
section.
|
section.
|
||||||
|
|
||||||
## Considered Options
|
## Considered Options
|
||||||
@@ -52,7 +58,7 @@ section.
|
|||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
**A home-scoped module owns the directory it declares: its existence, owner and mode.** The host creates
|
**A module that declares a directory under a home owns that directory: its existence, owner and mode.** The host creates
|
||||||
it if absent, owned by the account, and never removes it while it holds anything
|
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
|
([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
|
is in exactly one of four classes, and **the class is visible in the definition from the shape
|
||||||
@@ -105,7 +111,7 @@ finished its definition.
|
|||||||
| An owned file found with no record is kept once, then written | host tests of ADR 0102's kept-original rule |
|
| 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 |
|
| 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 |
|
| 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) |
|
| Every path a module touches under a home 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
|
## References
|
||||||
|
|
||||||
|
|||||||
+24
@@ -152,6 +152,30 @@ the node is bound to, and refuses with a notification otherwise.
|
|||||||
| 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 |
|
| 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 |
|
| 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 |
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-03, by [ADR 0193](0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md)
|
||||||
|
> and [ADR 0198](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md).**
|
||||||
|
> What stands: one manager holding the seat, one rotation source, a token sealed to the receiving
|
||||||
|
> module's key on request/reply and never an event, the agent module alone writing what the agent
|
||||||
|
> reads, the identity guard, the host knowing nothing. What moved: both modules' code is bundles the
|
||||||
|
> node's runtime launches over stdio and is the bus for — `mesh/ask` for a call made on the module's
|
||||||
|
> behalf, `mesh/publish` and `mesh/subscribe` beside it — so neither holds a bus credential of its own.
|
||||||
|
> The manager's refresh and visits are a long-running bundle the control node's runtime launches. And,
|
||||||
|
> by the operator's direction, **the manager starts every exchange**: it asks each bound node's agent
|
||||||
|
> module for its public key, hands it a token, asks it for a login waiting to be adopted, and reconciles
|
||||||
|
> every node on a schedule — which is what "the agent module asks the seat for its current token" and
|
||||||
|
> "offers the grant to the manager" in the decision above now mean in practice. The agent module could
|
||||||
|
> ask through its runtime; it does not need to.
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-04, by [ADR 0206](0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md).**
|
||||||
|
> What stands: the manager holding the seat, one rotation source, the grants encrypted in its store, a
|
||||||
|
> token sealed to the receiving module's key on request/reply and never an event, the agent module alone
|
||||||
|
> writing what the agent reads, the identity guard, bindings as a person's act. What moved: the dated note
|
||||||
|
> above — the manager no longer starts every exchange. Each node reports what it holds as state, without
|
||||||
|
> the secret; the manager asks a node for its grant only when a report shows one it does not hold, adopts
|
||||||
|
> a licence by refreshing it rather than into a licence configured beforehand, and keeps what each
|
||||||
|
> consumer should hold as state, from which the node fetches its token by request. The rotation and switch
|
||||||
|
> events are gone.
|
||||||
|
|
||||||
## References
|
## 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 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
|
||||||
|
|||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-04
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 203. The account's environment is one module's, and every module contributes to it
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
A variable or a `PATH` entry is a fact about the operator's account. A toolchain needs its directory
|
||||||
|
on `PATH`, a version manager needs a variable naming its directory, an agent needs a variable that
|
||||||
|
turns one of its behaviours off, and the shell sets an editor. Today every one of these is a line of
|
||||||
|
one shell's syntax in one hand-written startup file. [Research 025](../01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md)
|
||||||
|
measured on four machines:
|
||||||
|
|
||||||
|
- about a quarter of the 65 lines a workstation runs at shell start are environment;
|
||||||
|
- written into `.zshrc`, that environment reaches only interactive zsh. It misses the login shell's
|
||||||
|
`execute` verb ([ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)),
|
||||||
|
every script, and every program a graphical session starts;
|
||||||
|
- the service manager's place for the account's environment, `~/.config/environment.d/`, holds
|
||||||
|
nothing on any machine.
|
||||||
|
|
||||||
|
The shell module as first written carried some of these lines in its own block and dropped the rest.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Each module writes its own lines into the shell's startup file.** Rejected: one shell's syntax,
|
||||||
|
read by one kind of start, and the same lines rewritten by every shell module.
|
||||||
|
2. **Contribute the facts to the login shell, whose holder renders them.** Rejected: the environment
|
||||||
|
then depends on which module holds the shell, every shell module renders the same facts again, and
|
||||||
|
the graphical session sees nothing.
|
||||||
|
3. **Option 2, and the service manager's holder renders the same facts a second time** into
|
||||||
|
`environment.d`. Rejected: one fact set with two owners, whose renderings can disagree, and a
|
||||||
|
duty for the service manager unrelated to managing services.
|
||||||
|
4. **One file in `environment.d` syntax, sourced by shells.** Rejected: that syntax is close to
|
||||||
|
POSIX assignment but not equal, and a value one reader accepts breaks the other.
|
||||||
|
5. **Shells read the service manager's environment generator.** Rejected: every shell start then
|
||||||
|
runs a process and depends on the service manager, and the output is unquoted.
|
||||||
|
6. **A module of its own holds the environment.** One mesh seat, held by one module per node,
|
||||||
|
whose files are the account's environment. Every module contributes facts to it, and those facts
|
||||||
|
are written in each reader's format. Chosen. It was the operator's proposal.
|
||||||
|
|
||||||
|
Within option 6, two ways to write the files:
|
||||||
|
|
||||||
|
- **a. The holder's own code renders what it receives.** This was research 025's starting position.
|
||||||
|
Rejected: the code needs something to run it whenever a contribution changes, and the result exists
|
||||||
|
only after a machine has applied and run it.
|
||||||
|
- **b. The controller renders the facts into the holder's files,** in two named formats, at
|
||||||
|
composition. Chosen. The result is in the declaration before any machine applies it, nothing has to
|
||||||
|
trigger anything, and the two formats are standards: POSIX shell assignment and the service
|
||||||
|
manager's `environment.d`. The controller learns no shell. It writes an assignment in a standard
|
||||||
|
syntax, as it already writes a fail2ban stanza a module supplied.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. The environment is a node seat, `node-environment`, in the mesh's own set.** One module per node
|
||||||
|
holds it, and it is the only writer of the account's environment. The first holder is a module of its
|
||||||
|
own (working name `node-env`), with no package and no process.
|
||||||
|
|
||||||
|
**2. Any module contributes to it with `environment`:**
|
||||||
|
|
||||||
|
- **`variables`:** names and values. A name is a POSIX variable name and never `PATH`. A value is
|
||||||
|
literal; it may use `${machine:…}`, resolved first, and may not contain `$`, a quote, a backslash or
|
||||||
|
a line break. The only expansion is the mesh's own, so the two formats cannot read one value
|
||||||
|
differently.
|
||||||
|
- **`path`:** entries, each placed at the `start` or the `end` of the account's `PATH`.
|
||||||
|
|
||||||
|
**3. The holder places the rendered environment with two placeholders** in its own files:
|
||||||
|
|
||||||
|
- **`${environment:posix}`** renders lines a POSIX shell sources:
|
||||||
|
- every variable exported;
|
||||||
|
- every `PATH` entry added only if missing, so sourcing twice changes nothing.
|
||||||
|
- **`${environment:systemd}`** renders the same facts as the service manager's user environment, with
|
||||||
|
the account's existing `PATH` kept between the start and the end entries.
|
||||||
|
|
||||||
|
Each rendered line names the module that contributed it, so the file answers *where did this come
|
||||||
|
from*. Contributions are ordered by module name, and then in the order a module declared them.
|
||||||
|
|
||||||
|
**4. The seat's protocol fixes where the POSIX file is:** `~/.config/mesh/environment.sh` under the
|
||||||
|
account's home. A shell sources that path without knowing which module wrote it. The service
|
||||||
|
manager's file is `~/.config/environment.d/50-mesh.conf`.
|
||||||
|
|
||||||
|
**5. Refused at composition:**
|
||||||
|
|
||||||
|
- two modules on one node setting the same variable, both named;
|
||||||
|
- an environment placeholder in a module that does not claim `node-environment`.
|
||||||
|
|
||||||
|
A node with contributions and no holder writes them nowhere. The holder's absence is visible in the
|
||||||
|
node's assignments, and no contributor is refused for it, because a missing `PATH` entry is a gap, not
|
||||||
|
a broken machine.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- A shell's part in the environment is one line in its always-read startup file, sourcing the
|
||||||
|
POSIX file. A second shell module writes the same line in its own syntax, and no contributor
|
||||||
|
changes when the login shell does.
|
||||||
|
- The graphical session sees the same `PATH` as the terminal, from the same facts.
|
||||||
|
- The controller gains one gathered field and two renderers. Both are tested byte for byte, like
|
||||||
|
the jails a node composes ([to-be 31](../03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md)).
|
||||||
|
- What a person sets for themselves stays theirs: variables of their own sit in their own lines of
|
||||||
|
their shell's file, read after the mesh's.
|
||||||
|
- **What got harder:** a value that needs another variable expanded (`$HOME`, `$XDG_CONFIG_HOME`)
|
||||||
|
must be written with the mesh's own `${machine:…}` facts, or it is refused. Expansion at shell start
|
||||||
|
is exactly what made one value mean two things in two readers.
|
||||||
|
- Once [issue 168](../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)
|
||||||
|
closes, a value a person varies becomes a setting of the module that contributes it
|
||||||
|
([ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)).
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| Both renderings, byte for byte, from a fixed set of contributions | the controller's environment tests |
|
||||||
|
| Sourcing the POSIX rendering twice leaves `PATH` unchanged | the same tests, running `sh` over the rendering |
|
||||||
|
| A variable set by two modules is refused, naming both | the controller's resolve test |
|
||||||
|
| An environment placeholder outside the holder is refused | the catalogue check, which registration runs |
|
||||||
|
| A value with `$`, a quote, a backslash or a line break is refused | the manifest's parse test |
|
||||||
|
| The zsh holder sources the path the seat fixes | the catalogue's zsh test |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Research 025](../01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md)
|
||||||
|
- [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md),
|
||||||
|
[ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md),
|
||||||
|
[ADR 0177](0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md),
|
||||||
|
[ADR 0182](0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
||||||
|
- [To-be 41](../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md)
|
||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-04
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 204. A module contributes shell code to the login shell in named slots, and the login shell is the mesh's seat
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Some of what a shell runs at start is code in that shell's own syntax, and it belongs to other
|
||||||
|
modules:
|
||||||
|
|
||||||
|
- a prompt theme loads itself and its configuration;
|
||||||
|
- plugins load themselves;
|
||||||
|
- a version manager sources its loader.
|
||||||
|
|
||||||
|
Order matters: a prompt's instant-prompt cache must run first, and syntax highlighting last. The
|
||||||
|
predecessor kept all of this in one file per machine, and installed the theme and plugins by cloning
|
||||||
|
them in a hook.
|
||||||
|
[Research 025](../01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md) measured that file
|
||||||
|
as byte-identical on four machines. It carries:
|
||||||
|
|
||||||
|
- the shell's defaults;
|
||||||
|
- code belonging to four other pieces of software;
|
||||||
|
- a handful of the operator's own lines.
|
||||||
|
|
||||||
|
Nothing gave the other pieces a way in.
|
||||||
|
|
||||||
|
Two further facts bear on the seat itself:
|
||||||
|
|
||||||
|
- **The seat is declared by the zsh module** ([ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)
|
||||||
|
§1, under [ADR 0126](0126-a-module-declares-its-own-seats.md)). The controller refuses a second
|
||||||
|
module declaring a seat name, so fish or bash could only ever claim it, and the seat exists only
|
||||||
|
while zsh's definition is registered.
|
||||||
|
- **The kept region is the other way round.** [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)
|
||||||
|
describes a kept region as a marked block holding the operator's lines. The host built the inverse:
|
||||||
|
the mesh's region is the marked block, and every byte outside it is kept, verified unchanged, and
|
||||||
|
given back when the module goes.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **A drop-in directory** that each module places a file in, and the shell sources. Rejected: every
|
||||||
|
contributor names a path inside the shell module's territory
|
||||||
|
([ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md)); order becomes a naming
|
||||||
|
convention nothing checks; and nothing ties the file to the shell actually being the one it is
|
||||||
|
written for.
|
||||||
|
2. **Facts the holder renders**, through `contributes` / `receives`. Rejected: code is not a fact,
|
||||||
|
and the holder would only paste it.
|
||||||
|
3. **Code contributed for a named shell in a named slot, assembled by the controller into the
|
||||||
|
holder's file.** This is what the controller already does for fail2ban jails: each module supplies
|
||||||
|
text in the tool's own format, and the controller sorts and concatenates it into the holder's file
|
||||||
|
without interpreting it. Chosen.
|
||||||
|
|
||||||
|
For order, numbers (`10`, `50`, `90`) were rejected: every contributor guesses one, and collisions are
|
||||||
|
silent. **Three named slots** were chosen: `first`, `normal`, `last`.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. `node-login-shell` is a node seat in the mesh's own set,** with the verb `execute`. It replaces
|
||||||
|
the module-declared `login-shell`. Everything else ADR 0176 decided stands: the holder sets the
|
||||||
|
account's login shell through the `user` shape, `execute` is the contract, and any node may call it.
|
||||||
|
A shell module claims the seat; none declares it.
|
||||||
|
|
||||||
|
**2. Any module contributes shell code with `shell`:** entries naming the shell they are for (`zsh`,
|
||||||
|
`bash`, `fish`), the slot, and the code. The controller does not read the code.
|
||||||
|
|
||||||
|
**3. The holder places the code with placeholders** in its own files: `${shell:<shell>:<slot>}`. Each
|
||||||
|
is filled with that shell's code for that slot, from every module on the node:
|
||||||
|
|
||||||
|
- ordered by module name;
|
||||||
|
- each piece preceded by a line naming its module;
|
||||||
|
- empty when nothing is contributed.
|
||||||
|
|
||||||
|
A shell-code placeholder in a module that does not claim `node-login-shell` is refused.
|
||||||
|
|
||||||
|
**4. The holder's duties, which are the seat's protocol:**
|
||||||
|
|
||||||
|
- Source the account's environment ([ADR 0203](0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md))
|
||||||
|
from the startup file every start of that shell reads. For zsh that is `.zshenv`, which a script, a
|
||||||
|
login and `execute` all read.
|
||||||
|
- Write its interactive block at the **start** of the interactive startup file, so the operator's own
|
||||||
|
lines run after the mesh's and win.
|
||||||
|
- Run `execute` as a non-interactive login shell in the account's home:
|
||||||
|
- bounded below the runtime's call limit;
|
||||||
|
- its output bounded;
|
||||||
|
- its whole process group ended on timeout.
|
||||||
|
|
||||||
|
**5. The marked block is the mesh's; everything outside it is the operator's.** This is how ADR 0174's
|
||||||
|
"kept region" is built. That record keeps its decision and gains a note saying where the mechanism
|
||||||
|
lives.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- A prompt, a plugin and a version manager are each a module with its own package or archive, its own
|
||||||
|
configuration file, and a contribution. Assigning one adds its line to the shell, and unassigning it
|
||||||
|
takes the line away at the next composition.
|
||||||
|
- Assigning the shell module loses nothing the machine does today:
|
||||||
|
- what is common to every machine becomes the shell module's default or another module's
|
||||||
|
contribution;
|
||||||
|
- what is the operator's stays below the block.
|
||||||
|
- **The one-off migration is a person's act**, listed in the shell module's documentation
|
||||||
|
([ADR 0182](0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)):
|
||||||
|
delete the lines the block now carries from the found file.
|
||||||
|
- `login-shell.execute` becomes `node-login-shell.execute`. Nothing has called it yet; the shell
|
||||||
|
module was never assigned.
|
||||||
|
- **What got harder:** a module wanting a line in the shell must say which shell and which slot, and a
|
||||||
|
module supporting three shells writes its code three times. That is the honest cost of code in
|
||||||
|
three syntaxes.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| Code lands in its slot, in module order, only for its shell | the controller's shell-contribution tests |
|
||||||
|
| A shell-code placeholder outside the holder is refused | the catalogue check |
|
||||||
|
| `node-login-shell` is the mesh's, and no module may declare it | the seat table's tests |
|
||||||
|
| The zsh block sits at the start, sources the environment from `.zshenv`, and holds the three slots | the catalogue's zsh test |
|
||||||
|
| `execute` is bounded in time and output and kills its process group | the zsh module's tool tests over real child processes |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Research 025](../01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md)
|
||||||
|
- [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md),
|
||||||
|
[ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md),
|
||||||
|
[ADR 0203](0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md),
|
||||||
|
[to-be 31](../03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md)
|
||||||
|
- [To-be 41](../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md)
|
||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-04
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# 205. Software the distribution does not package ships as a pinned archive of the module's own
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The prompt theme the operator uses is not in the distribution's repositories. Its two plugins and an
|
||||||
|
autocomplete plugin are. The predecessor installed all four by running `git clone` against their
|
||||||
|
upstream repositories from an install hook. That way:
|
||||||
|
|
||||||
|
- the version on a machine was whatever upstream's default branch held the day the hook ran;
|
||||||
|
- two machines set up a week apart could differ;
|
||||||
|
- a machine with no route to upstream failed its install.
|
||||||
|
|
||||||
|
The mesh already has a pinned, delivered form for a module's own files: an **archive artifact** built
|
||||||
|
from a directory of the module's source, delivered by the artifact store, unpacked by the host's
|
||||||
|
`archive` resource, and pinned by digest. One showcase module uses it.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Clone from upstream on the machine,** as the predecessor did. Rejected: unpinned, unreproducible,
|
||||||
|
and it needs upstream reachable from every machine.
|
||||||
|
2. **Build from the distribution's user repository.** Rejected: the host installs packages from the
|
||||||
|
distribution's own repositories. A user-repository build is a toolchain on every machine for one
|
||||||
|
theme.
|
||||||
|
3. **Vendor a pinned upstream release into the module's directory and ship it as the module's archive
|
||||||
|
artifact.** Chosen. The release and its version are named in the module, its licence travels with
|
||||||
|
it, and every machine gets the same bytes from the mesh's own store.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**A module whose software the distribution does not package carries a pinned upstream release in its
|
||||||
|
own source directory and ships it as an archive artifact.**
|
||||||
|
|
||||||
|
- The module's documentation names the upstream, the version and the licence.
|
||||||
|
- The host unpacks it with the `archive` resource into a directory the module owns.
|
||||||
|
- An upgrade is a change to the module, reviewed like any other.
|
||||||
|
|
||||||
|
Software the distribution *does* package is installed as a package; a vendored copy of it is
|
||||||
|
refused in review.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The catalogue grows by the size of what it vendors: 1.4 MB for the prompt theme at the pinned
|
||||||
|
release.
|
||||||
|
- Upstream's security fixes reach a machine only when somebody updates the module. That is the same
|
||||||
|
trade every pinned dependency makes, and it is visible: the version is in the module.
|
||||||
|
- **What got harder:** a vendored program that downloads more at run time, as the prompt theme does
|
||||||
|
for its git status helper, still fetches that part from upstream on first use. This record pins
|
||||||
|
what the mesh ships, not what the software fetches for itself. The module's documentation says so.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| The archive is pinned by digest | the host's declaration validation, which refuses an archive without one |
|
||||||
|
| The upstream, version and licence are named | review of the module's documentation; the module's test asserts the licence file is in the archive |
|
||||||
|
| Packaged software is not vendored | review |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md)
|
||||||
|
- [To-be 41](../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md)
|
||||||
+144
@@ -0,0 +1,144 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-04
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 206. A node reports the Anthropic grant it holds; the licence manager adopts a licence by refreshing it, and what each node should hold is the manager's state
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
|
||||||
|
made the licence manager a module holding the `anthropic-licence-manager` seat: one rotation source, the
|
||||||
|
long-lived grants in its own store, a short-lived token handed to a node sealed on request/reply, the
|
||||||
|
agent module alone writing what the agent reads. How the manager *learns* a licence, and who starts each
|
||||||
|
exchange, it left to a later shape, and three texts have since disagreed: ADR 0183 has a node register
|
||||||
|
its key and the manager adopt a login only into a licence the node is already bound to; its dated note
|
||||||
|
of 2026-10-03 has the manager start every exchange and visit every node on a schedule; the agent module
|
||||||
|
as built asks the seat for its token when an event says to, and pushes a login to the seat.
|
||||||
|
|
||||||
|
**The operator settled it on 2026-10-04, in the operator's own words:** the manager must hold the active refresh token;
|
||||||
|
whichever node a login happened on holds the latest one; every client publishes what its credentials
|
||||||
|
file holds, the manager sees a licence it does not own yet and takes it into its store, and from then on
|
||||||
|
rotates it and distributes the access token. A manager launched for the first time holds no licence and
|
||||||
|
accepts what the clients report. Several nodes report the same account — today the nodes are all logged in
|
||||||
|
to one personal account — and before the manager adopts a grant it must know the refresh token still
|
||||||
|
works.
|
||||||
|
|
||||||
|
Two facts bound how that is built:
|
||||||
|
|
||||||
|
- **A refresh token cannot be published.** Anything published on the bus is kept, and a secret never
|
||||||
|
enters a stream, sealed or not ([design 32](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §10).
|
||||||
|
A module's state is a stream too, and the runtime refuses a value carrying a field named like a
|
||||||
|
credential ([ADR 0201](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md);
|
||||||
|
refused live on 2026-10-04 for an `Authorization` header).
|
||||||
|
- **A refresh token can only be checked by using it.** No endpoint answers "is this refresh token
|
||||||
|
valid" without exchanging it, and an exchange is presumed to rotate it (ADR 0183: the predecessor
|
||||||
|
lost a licence to a reused one). Checking and adopting are therefore one act, and whoever checks
|
||||||
|
becomes the token's only live holder.
|
||||||
|
|
||||||
|
Since ADR 0201 the bus has the shape this needs: **state** every node sees, including one that joins
|
||||||
|
later or a manager that starts later, read whole on start and then watched.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Each node publishes its credentials file, the token included.** What the operator described,
|
||||||
|
literally. Rejected for the token only: it would sit in a stream every principal that reads the
|
||||||
|
bucket can read, for as long as the bucket keeps it, and the runtime refuses it anyway.
|
||||||
|
2. **The manager visits every node on a schedule and collects a waiting login** (ADR 0183's dated
|
||||||
|
note). Rejected: the manager must know every node in advance and poll it, a node that joins later
|
||||||
|
waits for the next visit, and "what does each node hold" lives nowhere anyone can read.
|
||||||
|
3. **Each node reports what it holds as state, without the secret; the manager asks for the secret
|
||||||
|
only when the report shows a grant it does not hold, and adopts by refreshing.** Chosen: the
|
||||||
|
operator's flow, with the one part that cannot be on the bus moved onto request/reply.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. Every agent module reports what its node holds, as its own state.** One key per node in the
|
||||||
|
module's `holdings` state: the account's identity as the agent's own state file names it (account id,
|
||||||
|
address, organisation), the kind, the refresh token's **fingerprint** and whether one is present at
|
||||||
|
all, the access token's fingerprint and expiry, the licence it was last handed, and when the credentials
|
||||||
|
file last changed. Written when the module starts — a node already logged in when the module is first
|
||||||
|
assigned reports at once — and again whenever the credentials file changes. **No token, ever**: a
|
||||||
|
fingerprint names a token without being one.
|
||||||
|
|
||||||
|
**2. A licence is an account, and the manager learns it from the reports.** The manager reads every
|
||||||
|
node's `holdings` at start and watches them. A report carrying a refresh token whose fingerprint the
|
||||||
|
manager does not hold is a **candidate**: for an account it has no licence for yet, a new licence; for
|
||||||
|
one it has, a login made since. A manager launched for the first time holds no licence and treats
|
||||||
|
every report as a candidate. An API key still enters only through the seat's `adopt` verb, from a file
|
||||||
|
on the manager's node.
|
||||||
|
|
||||||
|
**3. The secret travels only when asked for.** For a candidate, the manager calls that node's agent
|
||||||
|
module on request/reply, giving its own public key, and is answered with the grant sealed to that key
|
||||||
|
(ADR 0183's channel, unchanged).
|
||||||
|
|
||||||
|
**4. Adopting is refreshing.** The manager exchanges the candidate's refresh token at the vendor's
|
||||||
|
endpoint under its lease for that account. If the exchange succeeds, the grant it got back is the
|
||||||
|
licence's, stored encrypted, and the manager is from then on its only rotation source. If it fails, the
|
||||||
|
candidate is recorded dead, nothing is adopted, and the report says so. **Several nodes, one account:**
|
||||||
|
candidates for one account are tried newest login first; the first that refreshes is adopted, and the
|
||||||
|
manager does not exchange the others.
|
||||||
|
|
||||||
|
**5. A node holds an access token only, so the latest login wins.** A node bound to an adopted licence
|
||||||
|
is handed the access token and nothing else, and the agent module writes the credentials file without a
|
||||||
|
refresh token — so the agent on the node can never refresh it, and two refreshers never hold one grant.
|
||||||
|
A refresh token appearing in a node's file afterwards can therefore only be a person's login there; its
|
||||||
|
report makes it a candidate, and if it refreshes it replaces the licence's grant. That is the operator's
|
||||||
|
"whichever node a login happened on holds the latest one", made mechanical.
|
||||||
|
|
||||||
|
**6. What each consumer should hold is the manager's state.** One key per consumer in the manager's
|
||||||
|
`bindings` state: the licence, its kind, and a **generation** that increases with every rotation and
|
||||||
|
every switch. The agent module watches its own key; when the generation is newer than the one it
|
||||||
|
applied, it asks the seat's `current` verb for the token, sending its public key, and is answered sealed
|
||||||
|
(request/reply). A node that was away reads its key when it is back and asks once. The `licence.rotated`
|
||||||
|
and `licence.switched` events go: what they announced is now the state itself, and a node needs the
|
||||||
|
latest, not the history.
|
||||||
|
|
||||||
|
**7. A first binding follows the login.** When the manager adopts a licence from a node's report, a
|
||||||
|
node with no binding yet whose report names that account is bound to it. Every later change is a
|
||||||
|
person's act through `bind`, `switch` and `release`, as ADR 0183 says.
|
||||||
|
|
||||||
|
**8. The identity guard stands, on two sources.** The account a grant is filed under is the identity
|
||||||
|
the node read from the agent's own state. Where the vendor's answer to the refresh names the account,
|
||||||
|
the manager compares the two and refuses a mismatch with a notification; whether it names it is
|
||||||
|
measured when the manager is built, and the record of which source decided is kept in the audit.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The manager needs no configuration to start: launched on a mesh whose nodes are logged in, it adopts
|
||||||
|
every account they hold, one licence each, from the newest login that still refreshes.
|
||||||
|
- Every node's holding is readable by anyone allowed to read the state — the console, an agent, the
|
||||||
|
operator — without a token in sight, which is what `licence_status` on each node answered one at a
|
||||||
|
time.
|
||||||
|
- **What got harder:** adoption consumes the refresh token the node held. On a node whose grant was
|
||||||
|
adopted, the agent's own copy is dead from that moment; until the manager hands it an access token
|
||||||
|
(decision 6), the agent keeps the access token it already had, which lives hours. And a node whose
|
||||||
|
file still holds a refresh token after adoption — it was not handed one yet — is a second holder of a
|
||||||
|
dead grant, not a live one, so the rotation-source rule holds.
|
||||||
|
- A candidate whose refresh fails is not retried by the manager: a dead refresh token does not come
|
||||||
|
back. A person logs in again, and the new report is a new candidate.
|
||||||
|
- Nothing in the reports is secret, but they do say which account each node uses; readers of the state
|
||||||
|
are declared in manifests like any other.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| No report carries a token | the runtime refuses a credential-named field (ADR 0201's test); the agent module's test: a report built from a full credentials file holds fingerprints and identity only |
|
||||||
|
| A node already logged in reports at start | the agent module's test: with a credentials file present and unchanged, starting writes its `holdings` key |
|
||||||
|
| A candidate is adopted only by a successful refresh, newest login first, once per account | the manager's tests against a stub vendor: two reports for one account, the newer refreshes and is adopted, the older is never exchanged; a failing refresh adopts nothing and records the candidate dead |
|
||||||
|
| A node is handed an access token only | the agent module's test: the file it writes after a hand-over holds no refresh token |
|
||||||
|
| A newer generation is fetched once, by request | the agent module's test: a `bindings` change with a newer generation asks `current` once; an equal one asks nothing |
|
||||||
|
| No event carries a token, and none announces a rotation any more | the manager's test of everything it publishes |
|
||||||
|
| Live | the manager launched with no licence on a mesh whose four nodes are logged in to one account adopts one licence, binds the four nodes, and each node's file then holds an access token and no refresh token |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) — the manager, its seat and its channel, which this extends
|
||||||
|
- [ADR 0201](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md) — module state, and the refusal of a secret in it
|
||||||
|
- [design 32](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §10 — no secret in a stream
|
||||||
|
- [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, amended by this record
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-04
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 207. A module depends on the node seats that apply its resources
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
A module declares resources: packages, services, containers, files. Some of those are applied
|
||||||
|
through software on the machine that is itself a module:
|
||||||
|
|
||||||
|
- a service through the service manager;
|
||||||
|
- a package through the package manager;
|
||||||
|
- a container through the container runtime.
|
||||||
|
|
||||||
|
Until now nothing said so. A module carried a *capability* such as `service-manager` or
|
||||||
|
`package-manager`, which the host detects on the machine. A capability says the software is
|
||||||
|
installed. It does not say that a module of the mesh holds the role and answers for it.
|
||||||
|
|
||||||
|
The cost showed on 2026-10-04:
|
||||||
|
|
||||||
|
- **A networking module declared the service manager's own package.** The controller allows one
|
||||||
|
declaration of a resource per node, so the module that *is* the service manager could not declare
|
||||||
|
its package and had been written without it. The networking module's real relation to the service
|
||||||
|
manager, that it needs one held on its node, was nowhere.
|
||||||
|
- **The container runtime** has had this decided for its own case since ADR 0165 and ADR 0166
|
||||||
|
(proposed): a module that delivers a container needs the runtime seat held on its machine, derived
|
||||||
|
from the container resource, with no manifest field.
|
||||||
|
- **The operator's order for building the machines' modules** (to-be 42) is *the most core first*.
|
||||||
|
That is an order the mesh should enforce, not one a person should remember.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep capabilities as the only gate.** Rejected: a capability is a fact about the machine, not
|
||||||
|
about the mesh. Software installed by hand satisfies it, and nothing then answers for it.
|
||||||
|
2. **A manifest field per module naming the seats it needs.** Rejected: a module would restate what
|
||||||
|
its resources already say, and a module that adds a service but forgets the field passes.
|
||||||
|
3. **Derive the dependency from the resources,** as ADR 0165 already does for containers, and refuse
|
||||||
|
an assignment whose seats are not held on the node. Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. Three node seats apply resources,** each in the mesh's own set:
|
||||||
|
|
||||||
|
| resource | applied through | seat | first holder |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `service` | the service manager | `node-service-manager` (ADR 0177) | `systemd` |
|
||||||
|
| `package` | the package manager | `node-package-manager` (new) | `pacman` |
|
||||||
|
| `container` | the container runtime | `node-container-runtime` (ADR 0166) | `docker` |
|
||||||
|
|
||||||
|
`node-package-manager` is new and has no verbs yet. `node-container-runtime` is seeded now as ADR 0166
|
||||||
|
names it. Its verbs, and the host creating containers through its holder, stay with that record's
|
||||||
|
acceptance.
|
||||||
|
|
||||||
|
**2. A module depends on each seat its resources need.** The controller derives this from the
|
||||||
|
resource types the module declares. A module never states it.
|
||||||
|
|
||||||
|
**3. A dependency is met when any module assigned to the same node holds the seat,** the module
|
||||||
|
itself included. The holders of these seats declare resources of each other's kinds: the service
|
||||||
|
manager's package needs the package manager, and the package manager's timer needs the service
|
||||||
|
manager. They are therefore judged as the node's whole set of assignments, never one at a time.
|
||||||
|
|
||||||
|
**4. Where it is checked:**
|
||||||
|
|
||||||
|
- **At `assign`,** an assignment whose dependencies are unmet by the node's assignments, including the
|
||||||
|
new one, is refused. The refusal names each seat and the modules in the catalogue that can hold it.
|
||||||
|
- **At composition,** an unmet dependency on a node is **reported** in `status` until the three holders
|
||||||
|
are assigned to every node. Then it is **refused** like any unresolved requirement. The switch is one
|
||||||
|
line in the controller, made when `status` reports none.
|
||||||
|
|
||||||
|
**5. The mesh's own foundation is exempt.** These are the pieces genesis lays before any module exists:
|
||||||
|
the host, the private network and the bootstrap runtime. Their declarations are the installation's,
|
||||||
|
not a module's.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The order of to-be 42 becomes the mesh's: `systemd`, `pacman` and `docker` on a node before
|
||||||
|
anything that installs, runs or contains.
|
||||||
|
- **Two modules no longer declare one shared package to say they need it.** A component's module
|
||||||
|
(networkd's) declares what it configures and depends on the seat. The component's own package
|
||||||
|
belongs to the module that holds the seat.
|
||||||
|
- Capabilities stay what they are, facts about the machine, used where a module needs the machine to
|
||||||
|
be able to do something.
|
||||||
|
- **What got harder:** a module can no longer be tried on a node that lacks the core three. That is
|
||||||
|
the point.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| The dependency is derived from resources: a service, a package and a container each need their seat | the controller's resolve tests |
|
||||||
|
| A node whose assignments hold the seats passes; one missing a holder is refused at `assign`, naming the seat and its possible holders | the same tests, and `assign` live |
|
||||||
|
| Mutual dependencies among the holders resolve when they are assigned together | the same tests |
|
||||||
|
| Until the switch, an unmet dependency is reported in `status` and does not refuse a push | the controller's status test |
|
||||||
|
| Foundation declarations are exempt | the composition test with genesis's declarations |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0165](0165-container-runtime-is-what-a-machine-can-run-and-a-running-runtime-is-its-holders-health.md),
|
||||||
|
[ADR 0166](0166-the-container-runtime-is-a-node-seat-and-the-host-creates-containers-through-its-holder.md),
|
||||||
|
[ADR 0177](0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)
|
||||||
|
- [To-be 42](../03-DESIGN/01-to-be/42-the-machines-modules-in-order.md)
|
||||||
@@ -191,6 +191,7 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0189** — [The store keeps what the records name, and a maintenance step holds its writers still](0189-the-store-keeps-what-the-records-name.md)
|
- **0189** — [The store keeps what the records name, and a maintenance step holds its writers still](0189-the-store-keeps-what-the-records-name.md)
|
||||||
- **0190** — [A seat's work is shared by its holders, and building is the first such role](0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md)
|
- **0190** — [A seat's work is shared by its holders, and building is the first such role](0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md)
|
||||||
- **0202** — [A provider declares what it derives for each consumer, and the mesh tells both ends](0202-a-provider-declares-what-it-derives-for-each-consumer.md)
|
- **0202** — [A provider declares what it derives for each consumer, and the mesh tells both ends](0202-a-provider-declares-what-it-derives-for-each-consumer.md)
|
||||||
|
- **0207** — [A module depends on the node seats that apply its resources](0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md)
|
||||||
|
|
||||||
### Its tiers, from the bottom up
|
### Its tiers, from the bottom up
|
||||||
|
|
||||||
@@ -301,6 +302,10 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0197** — [Every tool announces itself on the bus, in the NATS services protocol](0197-every-tool-announces-itself-on-the-bus-in-the-nats-services-protocol.md)
|
- **0197** — [Every tool announces itself on the bus, in the NATS services protocol](0197-every-tool-announces-itself-on-the-bus-in-the-nats-services-protocol.md)
|
||||||
- **0198** — [A module's long-running code is launched by the node's runtime, and reaches the bus through it](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)
|
- **0198** — [A module's long-running code is launched by the node's runtime, and reaches the bus through it](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)
|
||||||
- **0201** — [A module keeps its current state in key-value buckets it declares, and reaches them through the runtime](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)
|
- **0201** — [A module keeps its current state in key-value buckets it declares, and reaches them through the runtime](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)
|
||||||
|
- **0203** — [The account's environment is one module's, and every module contributes to it](0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md)
|
||||||
|
- **0204** — [A module contributes shell code to the login shell in named slots, and the login shell is the mesh's seat](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md)
|
||||||
|
- **0205** — [Software the distribution does not package ships as a pinned archive of the module's own](0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md)
|
||||||
|
- **0206** — [A node reports the Anthropic grant it holds; the licence manager adopts a licence by refreshing it, and what each node should hold is the manager's state](0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md)
|
||||||
|
|
||||||
### How it is built
|
### How it is built
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: designed
|
status: designed
|
||||||
code: []
|
code: []
|
||||||
updated: 2026-10-02
|
updated: 2026-10-04
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md
|
||||||
- 02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.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/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/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
|
||||||
@@ -54,8 +55,10 @@ instruction file and the manager's tools:
|
|||||||
| the console's entry in the agent's user-scope state | the managed settings' tool-server key, from the console's provision (§4) |
|
| 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)
|
**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
|
the module owns the directory `~/.claude` — that it exists, that the operator owns it, its mode,
|
||||||
module's own code writes for a subscription licence (§5). The person's memory, history, projects, local
|
`0700` — and declares it, so the mesh refuses a second module owning it. Of what is inside, it owns only
|
||||||
|
the agent's credentials file, which its own code writes for a subscription licence (§5); every other path
|
||||||
|
is *found*. 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
|
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
|
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.
|
lists them, and until they go the agent reads stale instructions beside the mesh's.
|
||||||
@@ -63,9 +66,12 @@ lists them, and until they go the agent reads stale instructions beside the mesh
|
|||||||
## 2. What the module declares and what its code writes
|
## 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
|
**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
|
in that directory carrying the node's name and the console's endpoint, and a settings file carrying the
|
||||||
settings; the bus, the console's provision, and that it uses the `anthropic-licence-manager` seat.
|
role and the extra tool servers, merged from the module's settings layers — the bundle is told the two
|
||||||
Nothing under the home, nothing under `/etc`.
|
files' paths, because a bundle's words are paths and constants only (ADR 0192); the bus, the console's provision, and that it uses the `anthropic-licence-manager` seat.
|
||||||
|
Two directories, declared so the ownership check sees them: the agent's managed directory under
|
||||||
|
`/etc`, root's, and `~/.claude` under the operator's home, the operator's. No *file* resource under
|
||||||
|
either: what is in them is written by the module's code (§2 below) or is the person's.
|
||||||
|
|
||||||
**Written by the module's code**, from the facts file and the manager's hand-over, whenever either
|
**Written by the module's code**, from the facts file and the manager's hand-over, whenever either
|
||||||
changes:
|
changes:
|
||||||
@@ -111,17 +117,20 @@ the playbooks in the record.
|
|||||||
|
|
||||||
## 4. The console
|
## 4. The console
|
||||||
|
|
||||||
The module tells the agent where the console is, and the port is the console's to say. **The console
|
> **Revised 2026-10-03, building it.** The vendor's managed-settings key for tool servers refuses any
|
||||||
provides a node-scoped provision** — its MCP endpoint on loopback — serving the port the machine gave
|
> URL that is not `https://`, including one on loopback, so it cannot carry the console. The module
|
||||||
it, and the module requires it. A requirement names what the consumer is coupled to
|
> owns the vendor's **exclusive** managed tool-server file instead (operator's choice): the console as
|
||||||
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)); co-location
|
> `mesh`, over HTTP on loopback, and every server in the module's `mcp_servers` setting — and no other.
|
||||||
resolves it; a machine without the console refuses the module by name. [To-be 34](34-the-console.md) is
|
> A server added by hand, a project's own file and a plugin's servers stop loading; claude.ai's
|
||||||
amended in the same change; issue 192 (open) found the gap.
|
> connectors are kept by a managed setting. A person's own servers move into the setting, for the mesh
|
||||||
|
> or for one node. Stdio straight to the bus, through the runtime's own client, was weighed and left
|
||||||
|
> for later: it needs a verb the delivered runtime does not have, and a session holding its own bus
|
||||||
|
> connection breaks on a credential rotation.
|
||||||
|
|
||||||
**Other tool servers** a person wants on every machine, or on one, are a declared setting of this module
|
**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`,
|
— mesh layer or node layer — rendered into the same managed file. The person sets them with the
|
||||||
validates a server and sets the setting through the controller's settings verb, so the list stays
|
controller's `settings` verb on this module, so the list stays declared state; the list is the operator's
|
||||||
declared state. The agent's own HTTP-only constraint for managed servers applies; a person's local
|
choice, set where every setting is set. 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.
|
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
|
**The entry's name is `mesh`.** The hand-made entry both workstations carry today is named after this
|
||||||
@@ -131,36 +140,42 @@ it is the person's to remove, and until then the agent sees the mesh's tools twi
|
|||||||
## 5. The licence: the consumer side
|
## 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)
|
[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:
|
decides it and [ADR 0206](../../02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md) says how it moves; 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;
|
- **makes a keypair** in its state the first time it runs, and sends the public half with every request
|
||||||
- **serves `apply`**: the manager's hand-over, a token sealed to the module's key, with the licence's
|
that is answered sealed;
|
||||||
name and kind. A rotation of the same licence is applied only if newer within one lineage; a switch is
|
- **reports what the node holds**, as its own `holdings` state, one key for this node: the account's
|
||||||
applied regardless, because across licences the expiries are unrelated. The answer says applied or
|
identity read from the agent's state file, the kind, the refresh token's fingerprint and whether one is
|
||||||
refused and why, and never echoes a token;
|
present, the access token's fingerprint and expiry, the licence and generation it last applied, when the
|
||||||
- **pulls** at start and when its token nears expiry, by the seat's `current` verb, and keeps the last
|
credentials file last changed. Written at start — a node already logged in reports at once — and on every
|
||||||
token when the manager does not answer, saying so;
|
change of the file. Never a token: the runtime refuses one anyway;
|
||||||
- **writes** for a subscription licence the credentials file as the operator, access-token-only; for the
|
- **hands over a grant only when asked**: `claude_code_grant` answers the manager, which gives its public
|
||||||
API-key licence sets the key-helper in the managed settings to a small program that prints the key
|
key, with the full grant in the credentials file sealed to that key — the one time a refresh token
|
||||||
from the module's state, so no file under the home is touched;
|
leaves the node, for the manager to adopt by refreshing it;
|
||||||
- **offers a login to the manager**: when the credentials file changes by a person's login, it reads the
|
- **watches the manager's `bindings` state** for this node, and when the generation is newer than the one
|
||||||
account's identity from the agent's state file and offers the grant to the seat, sealed to the manager's
|
it applied, asks the seat's `current` verb for the token, sealed to its own key. A rotation of the same
|
||||||
key, for adoption; the manager decides;
|
licence is applied only if newer within one lineage; a switch is applied regardless;
|
||||||
- **serves `licence_status`**: which licence and kind this node holds, when the token expires, whether
|
- **writes** for a subscription licence the credentials file as the operator, **access-token-only** — so
|
||||||
|
the agent here never refreshes, and a refresh token appearing later is a person's login, reported like
|
||||||
|
any change; 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;
|
||||||
|
- **serves `claude_code_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.
|
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
|
Switching is the seat's `switch` verb, asked through the console; this module only applies what the
|
||||||
handed.
|
state says it should hold. *2026-10-04:* this replaces the manager's visits of 2026-10-03 (ADR 0183's dated
|
||||||
|
note): the node reports, the manager asks for a secret only when a report shows one it does not hold, and
|
||||||
|
a token is fetched by request when the state says it changed.
|
||||||
|
|
||||||
## 6. Scope, settings and the order of assignment
|
## 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)).
|
**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:**
|
All four nodes carry one since 2026-10-03. **Per node:** the role. **Per mesh or per node:**
|
||||||
extra tool servers. **Prerequisite:** the manager holds its seat and has adopted the licences.
|
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
|
**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
|
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
|
new session read to confirm it sees the mesh's instruction file, the console's five tools under `mesh`, and
|
||||||
its licence; then the rest.
|
its licence; then the rest.
|
||||||
|
|
||||||
## 7. The package
|
## 7. The package
|
||||||
@@ -178,13 +193,13 @@ installer is rejected: it puts a self-updating binary under the person's home, i
|
|||||||
|
|
||||||
| Check | Defends |
|
| 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 |
|
| the module's definition names no node, path or login, declares no file under a home or `/etc` (only the two directories), 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 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 |
|
| 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 |
|
| 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 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 |
|
| 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 |
|
| a new session on the assigned workstation lists the console's five tools under `mesh` ([ADR 0195](../../02-DECISIONS/0195-the-meshs-tools-are-found-by-address-not-announced-whole.md)) and answers "which node am I" from the instruction file | the exit of the build |
|
||||||
|
|
||||||
## What this does not settle
|
## What this does not settle
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: in-progress
|
status: in-progress
|
||||||
code: [mesh-host, mesh-controller, mesh-tools, mesh-catalog]
|
code: [mesh-host, mesh-controller, mesh-tools, mesh-catalog]
|
||||||
updated: 2026-10-02
|
updated: 2026-10-04
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.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/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md
|
||||||
@@ -33,12 +33,16 @@ This is the design [to-be 29](29-a-node-has-operator-accounts.md) §2 called *a
|
|||||||
Worked on the first one, a shell. The `zsh` module declares:
|
Worked on the first one, a shell. The `zsh` module declares:
|
||||||
|
|
||||||
- a **package**, `zsh`;
|
- a **package**, `zsh`;
|
||||||
- **files under the home**, owned by the account: the shell's rc file with the module's default
|
- **files under the home**, owned by the account: the mesh's block at the start of the shell's rc
|
||||||
configuration, carrying a kept region for the operator's own lines, and `${setting:…}`
|
file with the module's default configuration and the slots other modules' code lands in, the
|
||||||
placeholders for the few values a node varies; the account and its home are machine facts the
|
operator's own lines kept after it, and `${setting:…}` placeholders for the few values a node
|
||||||
controller resolves ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md),
|
varies; the account and its home are machine facts the controller resolves
|
||||||
to-be 29 §2);
|
([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md),
|
||||||
- a **seat declaration**, `login-shell`, node-scoped, with its one verb; and a **claim** on it;
|
to-be 29 §2). Its environment is a contribution to the environment module, not lines of its own
|
||||||
|
([ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md),
|
||||||
|
[ADR 0204](../../02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md),
|
||||||
|
[to-be 41](41-the-shell-and-the-accounts-environment.md));
|
||||||
|
- a **claim** on the mesh's node-scoped seat `node-login-shell`, with its one verb;
|
||||||
- a **`user` shape** naming the shell, applied only where the module holds the seat;
|
- 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
|
- a **tools bundle**, the artifact kind for interpreted code, with `execute` and the module's own
|
||||||
`show-config`.
|
`show-config`.
|
||||||
@@ -80,7 +84,8 @@ root escalates itself.
|
|||||||
|
|
||||||
## 4. The seats of the environment
|
## 4. The seats of the environment
|
||||||
|
|
||||||
Decided now: **`login-shell`** (module-declared; zsh, fish, bash; verb `execute`) and
|
Decided now: **`node-login-shell`** (the mesh's own, ADR 0204; zsh, fish, bash; verb `execute`),
|
||||||
|
**`node-environment`** (the mesh's own, ADR 0203; the environment module; no verbs) and
|
||||||
**`node-service-manager`** (the mesh's own; systemd; verbs over units in both scopes). The rest
|
**`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),
|
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
|
one record each when its first holder is written: display server, display session, terminal
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ decisions:
|
|||||||
- 02-DECISIONS/0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md
|
- 02-DECISIONS/0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md
|
||||||
- 02-DECISIONS/0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md
|
- 02-DECISIONS/0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md
|
||||||
- 02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md
|
- 02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md
|
||||||
|
- 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 38. Building the operator's machine
|
# 38. Building the operator's machine
|
||||||
@@ -330,6 +331,13 @@ tool of each.
|
|||||||
|
|
||||||
## WP5 — The shell, on a server first
|
## WP5 — The shell, on a server first
|
||||||
|
|
||||||
|
*Replaced on 2026-10-04 by [to-be 41](41-the-shell-and-the-accounts-environment.md).* A review before
|
||||||
|
assigning found that the shell module would duplicate every machine's existing startup file, drop
|
||||||
|
lines from it, leave the prompt uninstalled, and could not be unassigned
|
||||||
|
([issue 228](../../04-ISSUES/228-a-login-the-mesh-set-is-never-given-back/00-report.md)). The shell,
|
||||||
|
its environment, the modules that plug into it, and the host's fix are built and proven there. What
|
||||||
|
follows is the original plan, kept for the record.
|
||||||
|
|
||||||
*mesh-catalog #224, already written. Half a day to assign and prove.*
|
*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"`
|
**Order.** Assign `zsh` to one server; push; `login-shell.execute@<server> command="uptime"`
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: designed
|
status: designed
|
||||||
code: []
|
code: []
|
||||||
updated: 2026-10-02
|
updated: 2026-10-04
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md
|
||||||
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
|
- 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/0024-model-access-is-a-provision.md
|
||||||
- 02-DECISIONS/0050-model-access-is-vendor-agnostic.md
|
- 02-DECISIONS/0050-model-access-is-vendor-agnostic.md
|
||||||
@@ -74,22 +75,22 @@ Carried from the predecessor, where each rule was earned by an incident:
|
|||||||
|
|
||||||
## 4. Handing a token to a node
|
## 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
|
*Amended 2026-10-04 by [ADR 0206](../../02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md)*, replacing the manager's visits: **what each consumer
|
||||||
runs. From then on:
|
should hold is the manager's state, and the token is fetched when it changes.**
|
||||||
|
|
||||||
- **On rotation**, the manager calls `claude-code.apply@<node>` on every node bound to the rotated
|
- **The manager keeps a `bindings` state**, one key per consumer: the licence, its kind, and a
|
||||||
licence, with the new token sealed to that node's module key. The module answers *applied*, or
|
**generation** that increases with every rotation and every switch. Nothing in it is secret.
|
||||||
*refused* and why, and the manager records it.
|
- **The agent module on each node watches its own key.** When the generation is newer than the one it
|
||||||
- **On a switch**, the same call with the other licence's token, and the binding is the authority: the
|
applied, it asks the seat's `current` verb, sending its public key, and is answered with the token
|
||||||
module applies a bind without comparing expiries, because across two licences the numbers are
|
sealed to it — request/reply, never an event. A node that was away reads its key when it is back and
|
||||||
unrelated.
|
asks once; a manager that is down leaves every node on its last token, which lives hours.
|
||||||
- **On a pull** — the module starting, or finding its token near expiry — the module calls the seat's
|
- **On a switch** the agent applies the new licence's token without comparing expiries, because across
|
||||||
`current` verb for its binding and is answered sealed the same way.
|
two licences the numbers are unrelated; within one licence it applies only a newer grant.
|
||||||
- **Never as an event.** What the manager emits names the licence and the outcome and carries no token.
|
- **No event announces a rotation or a switch.** What they announced is the state itself, and a node
|
||||||
|
needs the latest, not the history. What the manager still emits names an 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
|
A consumer that never asks is visible: its own report (§6) names the licence and generation it holds,
|
||||||
rather than falling silent. A node whose module refuses — a wrong identity, a stale grant within one
|
and a node behind its binding is drift the manager reports.
|
||||||
lineage — is recorded as drift and reported.
|
|
||||||
|
|
||||||
## 5. Who gets which licence
|
## 5. Who gets which licence
|
||||||
|
|
||||||
@@ -116,27 +117,46 @@ already keeps.
|
|||||||
|
|
||||||
## 6. Adopting a grant
|
## 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
|
*Amended 2026-10-04 by [ADR 0206](../../02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md)*: a licence is an account, learned from what the nodes
|
||||||
argument:
|
report, and adopted by refreshing it.
|
||||||
|
|
||||||
- **From a node's login.** A person logs in on a node, as they always have. The agent module there reads
|
- **Every node reports what it holds**, as the agent module's `holdings` state, one key per node: the
|
||||||
the account's identity from the agent's own state file, and offers the full grant to the seat sealed
|
account's identity read from the agent's own state file, the kind, the refresh token's fingerprint and
|
||||||
to the manager's key. The manager adopts it into the licence the node is bound to **only if the
|
whether one is present, the access token's fingerprint and expiry, the licence and generation it was
|
||||||
identity matches** that licence's recorded account; a licence not yet identified is identified by its
|
last handed, when the credentials file last changed. Written when the module starts — a node already
|
||||||
first adoption; a mismatch is refused and notified, because the predecessor once filed one account's
|
logged in reports at once — and on every change. Never a token.
|
||||||
grant into another's row this way.
|
- **The manager reads every report at start and watches them.** A report with a refresh token whose
|
||||||
|
fingerprint the manager does not hold is a candidate: a new licence for an account it has none for, a
|
||||||
|
login made since for one it has. A manager launched for the first time holds no licence and takes every
|
||||||
|
report as a candidate.
|
||||||
|
- **The secret is asked for, never published.** For a candidate the manager calls that node's agent
|
||||||
|
module, giving its own public key, and is answered with the grant sealed to it.
|
||||||
|
- **Adopting is refreshing.** The manager exchanges the candidate's refresh token under its lease for
|
||||||
|
that account; success makes the returned grant the licence's and the manager its only rotation source;
|
||||||
|
failure records the candidate dead and adopts nothing. Candidates for one account are tried newest login
|
||||||
|
first, and the first that refreshes ends the search — the others are never exchanged.
|
||||||
|
- **The latest login wins.** A bound node is handed an access token only and its file holds no refresh
|
||||||
|
token, so a refresh token appearing there later is a person's login; its report makes it a candidate,
|
||||||
|
and if it refreshes it replaces the licence's grant.
|
||||||
|
- **A first binding follows the login**: a node with no binding whose report names the adopted account
|
||||||
|
is bound to it. Every later change is `bind`, `switch` or `release`.
|
||||||
|
- **The identity guard** files a grant under the identity the node read; where the vendor's refresh
|
||||||
|
answer names the account too, a mismatch is refused and notified. Which source decided is audited.
|
||||||
- **An API key** is delivered to the manager by the operator through the seat's `adopt` verb from a file
|
- **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.
|
on the manager's node, never as an argument.
|
||||||
|
|
||||||
## 7. What it emits and serves
|
## 7. What it emits and serves
|
||||||
|
|
||||||
**Events**, no secret in any: `licence.rotated`, `licence.switched`, `licence.adopted`,
|
**Events**, no secret in any: `licence.adopted`, `licence.failing`, `licence.refused`, `usage.read` —
|
||||||
`licence.failing`, `licence.refused`, `usage.read` — the audit logger records them all.
|
the audit logger records them all. *2026-10-04 (ADR 0206):* `licence.rotated` and `licence.switched` are
|
||||||
|
gone; a rotation or a switch is a new generation in the `bindings` state.
|
||||||
|
|
||||||
|
**State**: `bindings`, which it keeps; the agent module's `holdings`, which it reads.
|
||||||
|
|
||||||
**The seat's verbs**, the contract every future holder must serve: `licences` (each with kind,
|
**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
|
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
|
or all), `usage` (current and history), `adopt`, and `current` (a consumer's token, sealed to the key the
|
||||||
consumer's token, sealed, asked by the consumer's module).
|
consumer sends — ADR 0206). The manager asks a node for a candidate grant by the agent module's own tool.
|
||||||
|
|
||||||
## 8. Settings
|
## 8. Settings
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
---
|
||||||
|
layer: to-be
|
||||||
|
status: designed
|
||||||
|
code: []
|
||||||
|
updated: 2026-10-04
|
||||||
|
decisions:
|
||||||
|
- 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md
|
||||||
|
- 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/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md
|
||||||
|
- 02-DECISIONS/0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md
|
||||||
|
- 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) gives the operator's machine.
|
||||||
|
|
||||||
|
*Revised 2026-10-03, after design 38's WP1–WP4b ran:* the node's tool runtime is live on all four
|
||||||
|
machines, tools are bundles it serves and each is given only the words its artifact declares, every
|
||||||
|
bundle is a child the runtime launches over stdio and is the bus for
|
||||||
|
([ADR 0193](../../02-DECISIONS/0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md),
|
||||||
|
[ADR 0198](../../02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)),
|
||||||
|
and the console offers five tools over addresses
|
||||||
|
([ADR 0195](../../02-DECISIONS/0195-the-meshs-tools-are-found-by-address-not-announced-whole.md)). What changed
|
||||||
|
in this plan: the wait on design 38's WP3 is over; the manager starts every exchange, by the operator's
|
||||||
|
direction (ADR 0183's dated note); and the manager's daemon is a long-running bundle the runtime launches,
|
||||||
|
which ADR 0198 decided the same day — nothing in this plan waits on another record.
|
||||||
|
|
||||||
|
## 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)).
|
||||||
|
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: one
|
||||||
|
workstation first for the agent, the control node first for the manager, then the rest. 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 out of the failure's reach, 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
|
||||||
|
|
||||||
|
Measured 2026-10-03 on the four machines and in the repositories.
|
||||||
|
|
||||||
|
| Piece | Today | Becomes |
|
||||||
|
|---|---|---|
|
||||||
|
| the node's tool runtime | live on all four, a host process; **runs as the operator account**; listens for the console on loopback at a port its own code fixes; launches or imports every assigned module's tools bundle and hands each its declared words | serves the agent module's tools; gains one provision for its endpoint (WP1) |
|
||||||
|
| the operator account | **stated on all four** — the runtime runs as it | read by the agent module from the runtime's own words |
|
||||||
|
| escalation | passwordless `sudo` for the operator account on all four — a fact about the machines, checked by nobody | how the agent module writes its managed directory under `/etc` |
|
||||||
|
| the agent itself | installed on all four, at four different versions, all above the one the managed tool-server key needs | declared as the module's package |
|
||||||
|
| a bundle's words | paths and constants written with `${dir:…}` and `${port:…}` only; a fact the mesh knows reaches a bundle as a file whose path is a word | the agent module's facts file and settings file |
|
||||||
|
| a bundle calling a tool | `mesh/ask` through the runtime that launched it (ADR 0198); no bundle holds a bus credential | how the manager visits every node |
|
||||||
|
| a module's own long-running code | a bundle the runtime launches and restarts (ADR 0198); the runtime's subscription and grants built, the modules moving in design 38 WP4c's waves | the manager's daemon (WP3, WP4) |
|
||||||
|
| the vendor's refresh, the sealed box, the grant file | `anthropic-manager` in the catalogue, built on the controller placement ADR 0183 moved away from; assigned to nothing | its client ported into the manager; the module retired (WP6) |
|
||||||
|
| the credentials write, the strip, the identity read | `anthropic-consumer` in the catalogue; tested; assigned to nothing | ported into the agent module with its tests; the module retired (WP6) |
|
||||||
|
| the predecessor's manager and consumer | the lease per licence, the expiry floor, the lineage comparison, the identity guard, three touchpoints, cooldowns | ported as logic with its tests |
|
||||||
|
|
||||||
|
## The order the work allows
|
||||||
|
|
||||||
|
```
|
||||||
|
WP0 the operator names the licences and each node's role (the live mesh) ── an hour
|
||||||
|
WP1 the runtime provides its endpoint (mesh-tools) ── small
|
||||||
|
WP2 the agent module (mesh-catalog) ──┐ WP2 needs WP1;
|
||||||
|
WP3 the manager's code, built and tested (mesh-catalog) ──┘ WP3 is independent
|
||||||
|
│
|
||||||
|
WP2 live: one workstation, configuration only — no licence yet ── the first live proof
|
||||||
|
│
|
||||||
|
WP4 the manager live on the control node ── its daemon a long-running bundle (ADR 0198)
|
||||||
|
WP5 the licence end to end on one workstation
|
||||||
|
WP6 the rest of the nodes, and the predecessor's remains
|
||||||
|
```
|
||||||
|
|
||||||
|
## WP0 — The operator names the licences and each node's role
|
||||||
|
|
||||||
|
*The live mesh. An hour, and it is the operator's.* The accounts are stated already. What remains: the
|
||||||
|
names of the two subscription licences and the API key; each node's role, as the agent module's setting
|
||||||
|
on the node layer once the module is registered.
|
||||||
|
|
||||||
|
**Proof.** The module's settings list a role for every node; the licences have names.
|
||||||
|
|
||||||
|
## WP1 — The runtime provides its endpoint
|
||||||
|
|
||||||
|
*mesh-tools. An hour.*
|
||||||
|
|
||||||
|
**What changes.** The `node-tools` manifest provides a node-scoped provision, `mcp-endpoint`, serving
|
||||||
|
the port its code listens on, the way the local model server serves its API
|
||||||
|
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)). Co-location
|
||||||
|
resolves it. The runtime's port stays what its code fixes; assigning it is
|
||||||
|
[issue 192](../../04-ISSUES/192-the-meshs-tools-reach-a-person-only-by-a-registration-made-by-hand/00-report.md)'s
|
||||||
|
second question and not this package's.
|
||||||
|
|
||||||
|
**Proof.** The plan for a workstation carrying a consumer of `mcp-endpoint` shows it bound to the
|
||||||
|
runtime's port; the controller's tests and the catalogue's checks pass.
|
||||||
|
|
||||||
|
## WP2 — The agent module
|
||||||
|
|
||||||
|
*mesh-catalog. A day and a half.*
|
||||||
|
|
||||||
|
**What is written**, as design 36 says:
|
||||||
|
|
||||||
|
1. **The manifest.** The agent's package. A state directory. A facts file in it, rendered by the mesh:
|
||||||
|
the node's name and the console's address from `mcp-endpoint`. A settings file in it, merged from
|
||||||
|
the module's settings layers: the node's role and the extra tool servers. A tools bundle whose
|
||||||
|
words name the two files, the state directory and nothing else. The two directories it owns declared — the agent's managed
|
||||||
|
directory and `~/.claude` — and **no file resource under either.**
|
||||||
|
2. **The renderer**, run whenever the runtime collects the module's tools: from the two files, the
|
||||||
|
managed settings file (the tool servers under the entry `mesh`, the attribution trailers, the
|
||||||
|
key-helper for an API-key binding) and the managed instruction file, written under the agent's
|
||||||
|
managed directory through the account's escalation, only when their content changed.
|
||||||
|
3. **The keypair**, made once in the state directory; X25519 and an authenticated cipher from the
|
||||||
|
language's own library, so the bundle carries no dependency.
|
||||||
|
4. **The tools and the state** (*2026-10-04, ADR 0206*): `claude_code_status` (what is rendered, what
|
||||||
|
licence is held, when its token expires, fingerprints only); `claude_code_render` (render now);
|
||||||
|
`claude_code_grant` (the full grant in the credentials file, sealed to the key the manager gives).
|
||||||
|
The `holdings` state, written at start and on every change of the credentials file; a watch of the
|
||||||
|
manager's `bindings` key for this node, which asks the seat's `current` on a newer generation and
|
||||||
|
applies the sealed answer — only if newer within one lineage unless it is a switch; the credentials
|
||||||
|
write as the operator, access-token-only, atomic; the key-helper program for an API key.
|
||||||
|
5. **The documentation**: the six predecessor files and the hand-made console entry a person removes.
|
||||||
|
|
||||||
|
**Proof, before anything runs live.** Unit tests: the renderer writes the mesh's keys and nothing else;
|
||||||
|
it writes nothing when nothing changed; the credentials write strips a refresh token and is atomic; the
|
||||||
|
lineage cases from the predecessor; a sealed hand-over opens only with the module's key; no tool's answer
|
||||||
|
contains a token. The catalogue's checks pass.
|
||||||
|
|
||||||
|
**Proof, live, on one workstation, configuration only.** Assign the module; set the node's role; push.
|
||||||
|
The agent's managed directory holds the two files; everything under the person's agent directory is
|
||||||
|
byte-identical to before; a new session lists the console's five tools under `mesh` and answers *which node am
|
||||||
|
I* from the managed instruction file. `claude_code_status` answers through the console. No licence is
|
||||||
|
touched: the module writes the credentials file only when it is handed a token.
|
||||||
|
|
||||||
|
## WP3 — The manager's code, built and tested
|
||||||
|
|
||||||
|
*mesh-catalog. Two to three days.*
|
||||||
|
|
||||||
|
**What is written**, as design 39 says: the manifest (the seat and its verbs, a database, a `secret`
|
||||||
|
for the key the grants are encrypted with, a tools bundle and a long-running bundle for the daemon, both launched by the runtime, settings
|
||||||
|
with defaults); the store's migrations; the refresh with its plan, lease, floor and cadence as pure
|
||||||
|
functions; the vendor client from `anthropic-manager`; adoption from a file and from a node's waiting
|
||||||
|
login with the identity guard; usage and its threshold; the seat's verbs. *2026-10-04 (ADR 0206):* in place
|
||||||
|
of the visit, the watch of every node's `holdings`, adoption of a candidate by refreshing it (newest login
|
||||||
|
first, once per account), the `bindings` state with a generation per consumer, and `current`.
|
||||||
|
|
||||||
|
**Proof, before anything runs live.** Unit tests: two refresh runs started together rotate one grant
|
||||||
|
once; a mismatching identity is refused; a worker bound to a dead licence is refused and never lent
|
||||||
|
another; nothing the daemon emits carries a token; a hand-over sealed for one node opens with no other
|
||||||
|
node's key.
|
||||||
|
|
||||||
|
## WP4 — The manager live on the control node
|
||||||
|
|
||||||
|
*The live mesh. Half a day.* The daemon is a long-running bundle the control node's runtime launches
|
||||||
|
(ADR 0198); it calls each node's agent module by `mesh/ask`. If the runtime's half of ADR 0198 is not
|
||||||
|
yet live on the control node when this package starts, this package waits for it: no tool container, no
|
||||||
|
credential copied by hand.
|
||||||
|
|
||||||
|
**Order.** Assign the manager on the control node; push. It reads every node's `holdings` and adopts each
|
||||||
|
account the nodes are logged in to, by refreshing the newest login's grant (ADR 0206); each node with no
|
||||||
|
binding is bound to the account it reported. Adopt the API key from a file there. A second subscription
|
||||||
|
account enters by a login on a workstation carrying the agent module.
|
||||||
|
|
||||||
|
**Proof.** Through the console: `anthropic-licence-manager.licences` lists three licences with identity
|
||||||
|
and expiry; within the cadence the audit shows a rotation and a later expiry; a forced `refresh` is
|
||||||
|
logged with the vendor's answer.
|
||||||
|
|
||||||
|
## WP5 — The licence end to end on one workstation
|
||||||
|
|
||||||
|
*The live mesh. Half a day. The proof of the whole.*
|
||||||
|
|
||||||
|
**Order.** Record the checksums under the person's agent directory. Bind the workstation to a
|
||||||
|
subscription licence. Remove the six predecessor files and the hand-made console entry. Start a session.
|
||||||
|
|
||||||
|
**Proof.** Everything under the person's agent directory is byte-identical but the credentials file,
|
||||||
|
which is owned by the operator, readable by nobody else, and names no refresh token. A session makes a
|
||||||
|
model request. `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, 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.* The module on every node, the predecessor's files removed on
|
||||||
|
the second workstation; a login under a licence's account collected and adopted, and one under the wrong
|
||||||
|
account refused and notified; `anthropic-manager` and `anthropic-consumer` retired from the catalogue;
|
||||||
|
designs 36 and 39 set to `implemented` with the as-is written
|
||||||
|
([playbook 02](../../00-META/process/02-graduation.md)).
|
||||||
|
|
||||||
|
**Proof.** `claude_code_status` answers on every node; the refusal'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). The four machines have the package; a fifth would refuse the module in its package manager's
|
||||||
|
words.
|
||||||
|
- **Escalation as a checked fact.** The agent module's write under `/etc` relies on the operator
|
||||||
|
account's passwordless `sudo`, true on all four and checked by nothing. A machine without it refuses
|
||||||
|
the render in the tool's own words; making escalation a reported capability is design 38's to decide.
|
||||||
|
- **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 knows them from WP3; the consumers do
|
||||||
|
not exist yet ([to-be 15](15-the-agent-session.md)).
|
||||||
|
- **Whether a refresh token is single-use.** WP4 may measure it; 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 WP2's live proof runs, design 36 moves to `in-progress` with its owning
|
||||||
|
repository; when WP5's does, design 39 does too; and when WP6's does, both move to `implemented`, with
|
||||||
|
the as-is written.
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
---
|
||||||
|
layer: to-be
|
||||||
|
status: in-progress
|
||||||
|
code: [mesh-host, mesh-controller, mesh-catalog]
|
||||||
|
updated: 2026-10-04
|
||||||
|
decisions:
|
||||||
|
- 02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md
|
||||||
|
- 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md
|
||||||
|
- 02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.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/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 41. The shell and the account's environment
|
||||||
|
|
||||||
|
What it takes for the operator's shell to be modules, without a machine losing anything it does
|
||||||
|
today. This design replaces the shell half of
|
||||||
|
[to-be 38](38-building-the-operators-machine.md) WP5, and finishes the service-manager module of WP6
|
||||||
|
short of its user-scoped units. The decisions are [ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md)
|
||||||
|
(the environment), [ADR 0204](../../02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md)
|
||||||
|
(shell code and the seat) and [ADR 0205](../../02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md)
|
||||||
|
(vendored software). The evidence is [research 025](../../01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md).
|
||||||
|
|
||||||
|
## What a node with a shell looks like
|
||||||
|
|
||||||
|
```
|
||||||
|
toolchain, agent, … prompt, plugins, version manager
|
||||||
|
│ environment │ shell (zsh, slot)
|
||||||
|
▼ ▼
|
||||||
|
node-environment ── holds ── node-env node-login-shell ── holds ── zsh
|
||||||
|
│ │
|
||||||
|
├─▶ ~/.config/mesh/environment.sh ◀─ sourced from zsh's block in ~/.zshenv
|
||||||
|
└─▶ ~/.config/environment.d/50-mesh.conf ◀─ read by the account's service manager
|
||||||
|
│
|
||||||
|
~/.zshrc: the mesh's block FIRST — defaults and the
|
||||||
|
three slots — then the operator's own lines, kept
|
||||||
|
```
|
||||||
|
|
||||||
|
**The environment module** (`node-env`) holds `node-environment`. It has no package and no process.
|
||||||
|
Its two files are written by the host from placeholders the controller fills.
|
||||||
|
|
||||||
|
**The shell module** (`zsh`) holds `node-login-shell`. It:
|
||||||
|
|
||||||
|
- installs the package;
|
||||||
|
- sets the login shell through the `user` shape;
|
||||||
|
- writes two blocks:
|
||||||
|
- one in `.zshenv`, sourcing the environment;
|
||||||
|
- one at the start of `.zshrc`, holding the defaults every machine shares today: the title, the
|
||||||
|
keybindings, the aliases and the two small functions, with the three slots in place;
|
||||||
|
- contributes its own environment: the editor, the configuration home, and `~/.local/bin` plus the
|
||||||
|
two script directories on `PATH`;
|
||||||
|
- serves `execute` and its own `zsh_config`.
|
||||||
|
|
||||||
|
**The prompt module** (`powerlevel10k`) ships the theme as a pinned vendored archive, and the prompt's
|
||||||
|
configuration as its own file in a directory it owns. It contributes the zsh code that loads both.
|
||||||
|
|
||||||
|
**Two plugin modules** (`zsh-autosuggestions`, `zsh-syntax-highlighting`) each install their
|
||||||
|
distribution package and contribute one line. Syntax highlighting goes in the `last` slot, which is
|
||||||
|
what its upstream asks for.
|
||||||
|
|
||||||
|
**What stays the operator's** is everything below the block in `.zshrc`, and `~/.zshrc.local`, which
|
||||||
|
the operator's lines source as they do today. On every machine today that means:
|
||||||
|
|
||||||
|
- the version manager's lines and the toolchain's `PATH` entry, until those modules exist;
|
||||||
|
- the two variables naming the operator's own script library;
|
||||||
|
- the agent's title variable;
|
||||||
|
- the port aliases;
|
||||||
|
- the workstation's desktop variables, which live in `~/.zshrc.local` already.
|
||||||
|
|
||||||
|
Nothing is lost at any step, because a line moves out of the operator's part only when a module
|
||||||
|
carries it.
|
||||||
|
|
||||||
|
**The migration is a person's act**, listed in the zsh module's documentation (ADR 0182): after the
|
||||||
|
first push, delete from `.zshrc` the lines the block now carries. Until then they run twice, which is
|
||||||
|
harmless and visible.
|
||||||
|
|
||||||
|
## Work packages
|
||||||
|
|
||||||
|
```
|
||||||
|
WP1 the host gives a login back (mesh-host) issue 228
|
||||||
|
WP2 the controller composes environment and shell code (mesh-controller)
|
||||||
|
WP3 the modules (mesh-catalog) needs WP2 to resolve
|
||||||
|
WP4 the service manager's module, finished (mesh-catalog) independent
|
||||||
|
WP5 assign and prove (operator-gated) needs WP1–WP3 merged and rolled
|
||||||
|
```
|
||||||
|
|
||||||
|
WP1, WP2 and WP4 are independent, and are built in parallel on one feature branch per repository
|
||||||
|
([playbook 07](../../00-META/process/07-feature-branches.md)). WP3 is written in parallel and proven
|
||||||
|
against WP2's controller before anything is published.
|
||||||
|
|
||||||
|
## WP1 — The host gives a login back
|
||||||
|
|
||||||
|
*mesh-host. Half a day. [Issue 228](../../04-ISSUES/228-a-login-the-mesh-set-is-never-given-back/00-report.md).*
|
||||||
|
|
||||||
|
**What changes.**
|
||||||
|
|
||||||
|
- The `user` shape records, in its applied record, the login shell it found whenever it changes it.
|
||||||
|
- Removing a `user` never deletes the account, whether or not the mesh created it. If the account's
|
||||||
|
shell is still the one the mesh set, and the recorded shell is still executable, the recorded shell
|
||||||
|
is set back. Otherwise the shell is left as it is, and the outcome says why.
|
||||||
|
- Before a shell is set, it is refused unless it is executable and listed among the machine's shells.
|
||||||
|
The exception is a shell that refuses logins (`nologin`, `false`): the distribution does not list
|
||||||
|
those, and the controller's own account uses one, so it need only be executable. The refusal fails
|
||||||
|
that resource and leaves the account untouched.
|
||||||
|
- A directory the host creates on the way to a file, a block or an archive inside an account's home
|
||||||
|
belongs to that account, the home itself included when the host makes it. A directory that was
|
||||||
|
already there keeps its owner and mode (ADR 0182). Until this, a fresh account's `~/.config` or
|
||||||
|
`~/.local/share` would have been created as root's.
|
||||||
|
- Giving the shell back is reported, never fatal. A failed `usermod` on removal is named in the
|
||||||
|
outcome and the record is dropped, because a fatal removal is exactly the wedge issue 228 is about.
|
||||||
|
|
||||||
|
**Proof.** The host's tests:
|
||||||
|
|
||||||
|
- an undeclared `user` no longer stops the apply;
|
||||||
|
- the found shell comes back;
|
||||||
|
- a shell changed by a person since is left alone;
|
||||||
|
- a missing shell is refused before `usermod` runs;
|
||||||
|
- a created account survives its removal.
|
||||||
|
|
||||||
|
## WP2 — The controller composes environment and shell code
|
||||||
|
|
||||||
|
*mesh-controller. One to two days.*
|
||||||
|
|
||||||
|
**What changes.**
|
||||||
|
|
||||||
|
- **The seat table.** It gains `node-environment` (node scope, no verbs) and `node-login-shell`
|
||||||
|
(node scope, the verb `execute`). A module may no longer declare a seat named `login-shell` or
|
||||||
|
`node-login-shell`. Both new seats are seeded into a live store by the existing additive seeding.
|
||||||
|
- **The manifest.** It gains two contribution fields, each refused at parse when malformed:
|
||||||
|
- `environment`, with `variables` and `path`: a variable name must be a POSIX name and not `PATH`;
|
||||||
|
a value may not contain `$`, a quote, a backslash or a line break; a path entry's place is
|
||||||
|
`start` or `end`;
|
||||||
|
- `shell`: each entry names a known shell, a known slot, and non-empty code.
|
||||||
|
- **Composition.** It fills `${environment:posix}`, `${environment:systemd}` and
|
||||||
|
`${shell:<shell>:<slot>}` in the claiming holder's file contents, from every module assigned to
|
||||||
|
the node. The rendering is ADR 0203's and ADR 0204's: module order, a naming line per contribution,
|
||||||
|
`PATH` entries added only when missing. `${machine:…}` in a contributed value is resolved first.
|
||||||
|
- **Refusals.** A variable set by two modules on one node is refused, naming both. A placeholder in a
|
||||||
|
module that does not claim the matching seat is refused, both at the catalogue check and at
|
||||||
|
composition.
|
||||||
|
|
||||||
|
**Proof.** The controller's tests:
|
||||||
|
|
||||||
|
- both environment renderings, byte for byte, from a fixed set of contributions;
|
||||||
|
- the POSIX rendering sourced twice by `sh` leaves `PATH` unchanged;
|
||||||
|
- slot order and per-shell filtering;
|
||||||
|
- each refusal, by name;
|
||||||
|
- the seat table carries both seats and refuses a module declaring either.
|
||||||
|
|
||||||
|
The catalogue check over the whole catalogue passes.
|
||||||
|
|
||||||
|
## WP3 — The modules
|
||||||
|
|
||||||
|
*mesh-catalog. One day.*
|
||||||
|
|
||||||
|
**What changes.**
|
||||||
|
|
||||||
|
- **`node-env`, new.** It claims `node-environment` and declares two owned files: the POSIX file at
|
||||||
|
the path the seat fixes, and the service manager's file, each holding its placeholder. It declares
|
||||||
|
no tools.
|
||||||
|
- **`zsh`, rewritten.**
|
||||||
|
- It drops its seat declaration and claims `node-login-shell`.
|
||||||
|
- Its environment moves to a contribution.
|
||||||
|
- It writes a `.zshenv` block that sources the environment file.
|
||||||
|
- Its `.zshrc` block goes at the start and carries today's shared defaults, with the three slots.
|
||||||
|
- It keeps the `user` shape.
|
||||||
|
- `execute` runs `zsh -lc` in the account's home, with the runtime's session words for the user
|
||||||
|
manager. Its timeout is bounded below the runtime's thirty-second call limit; its output is cut at
|
||||||
|
a bound and marked as cut; on timeout it kills the process group.
|
||||||
|
- Tests cover the tool over real child processes and the manifest's shape.
|
||||||
|
- Its documentation lists the one-off migration.
|
||||||
|
- **`powerlevel10k`, new.**
|
||||||
|
- The theme is vendored at a pinned upstream release, with its licence, as an archive artifact
|
||||||
|
unpacked into the module's directory under the account's home.
|
||||||
|
- The prompt configuration is today's file, as its own owned file in the same directory.
|
||||||
|
- It contributes the zsh code that loads the theme and the configuration.
|
||||||
|
- Today's file has the instant-prompt cache commented out, so the module does not turn it on.
|
||||||
|
- **`zsh-autosuggestions` and `zsh-syntax-highlighting`, new.** Each declares its package and
|
||||||
|
contributes its loader from the distribution's path, in the `normal` and `last` slots.
|
||||||
|
|
||||||
|
**Proof.** The controller's catalogue check over the whole catalogue passes. The modules' tests pass.
|
||||||
|
A rehearsal composition for a node holding all five shows:
|
||||||
|
|
||||||
|
- the `.zshrc` block with the prompt in `normal` and highlighting in `last`;
|
||||||
|
- the environment file with the shell's `PATH` entries;
|
||||||
|
- the service manager's file.
|
||||||
|
|
||||||
|
## WP4 — The service manager's module, finished
|
||||||
|
|
||||||
|
*mesh-catalog. Half a day. From the review of 2026-10-04.*
|
||||||
|
|
||||||
|
**What changes.**
|
||||||
|
|
||||||
|
- System-scope `start`, `stop`, `restart`, `enable` and `disable` escalate with `sudo -n` when the
|
||||||
|
runtime is not root, as the packet filter and intrusion modules do. They name a refusal by how it
|
||||||
|
failed.
|
||||||
|
- User scope reaches the account's manager by its runtime directory, which the runtime's environment
|
||||||
|
lacks.
|
||||||
|
- A failed `systemctl` is an error, not an empty list.
|
||||||
|
- The package resource goes: the service manager is always present, and it collided with the network
|
||||||
|
module's identical declaration on a machine running both.
|
||||||
|
- `status` says whether the mesh declares the unit. The restore note is attached only to such a unit.
|
||||||
|
- Tests cover a fake runner.
|
||||||
|
|
||||||
|
The user-scoped units of mesh-host #72 stay to-be 38's WP6.
|
||||||
|
|
||||||
|
**Proof.** The module's tests. Live, after WP5:
|
||||||
|
|
||||||
|
- `node-service-manager.units` answers in both scopes on a workstation and on a server;
|
||||||
|
- `restart` of a harmless unit answers `ok`.
|
||||||
|
|
||||||
|
## WP5 — Assign and prove
|
||||||
|
|
||||||
|
*Operator-gated. Nothing here runs without the operator's go-ahead.*
|
||||||
|
|
||||||
|
**Order.**
|
||||||
|
|
||||||
|
1. Merge WP1 and roll the host.
|
||||||
|
2. Merge WP2, and push the controller.
|
||||||
|
3. Merge WP3 and WP4, and build the new modules by hand: a new catalogue module's first build is asked
|
||||||
|
for, not automatic.
|
||||||
|
4. On one server, assign `node-env`, `zsh`, `zsh-autosuggestions` and `zsh-syntax-highlighting`, and
|
||||||
|
push. Then check:
|
||||||
|
- `node-login-shell.execute@<server> command="echo $PATH"` shows the shell's entries;
|
||||||
|
- `.zshrc` begins with the block;
|
||||||
|
- the operator's lines follow untouched;
|
||||||
|
- the environment file and the service manager's file exist.
|
||||||
|
5. The operator deletes the duplicated lines, per the zsh module's documentation.
|
||||||
|
6. The other server, then the two workstations, the workstations also with `powerlevel10k`.
|
||||||
|
7. Assign `systemd` everywhere, and prove WP4.
|
||||||
|
8. Unassign one plugin module on one machine. Its line leaves the block at the next push, and nothing
|
||||||
|
else changes.
|
||||||
|
|
||||||
|
The follow-up records to-be 38 names are still owed:
|
||||||
|
|
||||||
|
- what a shell module assigned beside the holder does;
|
||||||
|
- how a person's own environment variable is a setting rather than a line, once issue 168 closes.
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
---
|
||||||
|
layer: to-be
|
||||||
|
status: in-progress
|
||||||
|
code: [mesh-catalog, mesh-controller, mesh-host]
|
||||||
|
updated: 2026-10-04
|
||||||
|
decisions:
|
||||||
|
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
|
||||||
|
- 02-DECISIONS/0040-what-a-module-is.md
|
||||||
|
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||||
|
- 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
|
||||||
|
- 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
|
||||||
|
- 02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md
|
||||||
|
- 02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md
|
||||||
|
- 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md
|
||||||
|
- 02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md
|
||||||
|
- 02-DECISIONS/0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 42. The machines' modules, in order
|
||||||
|
|
||||||
|
The order in which the modules of [research 026](../../01-RESEARCH/026-the-graphical-session-as-modules/00-overview.md)
|
||||||
|
and [research 027](../../01-RESEARCH/027-the-system-layer-as-modules/00-overview.md) are built and rolled
|
||||||
|
out, as the operator set it on 2026-10-04.
|
||||||
|
|
||||||
|
Three phases, in order, each built from the bottom up (the most core module first):
|
||||||
|
|
||||||
|
1. the modules every machine shares;
|
||||||
|
2. those both workstations share;
|
||||||
|
3. those of one machine model.
|
||||||
|
|
||||||
|
The shell came first ([to-be 41](41-the-shell-and-the-accounts-environment.md)). Each module's
|
||||||
|
definition, improvements and tools follow the research. A position that needs a new mechanism (a new
|
||||||
|
seat, a gated assignment, generalised contributions) gets its record when its first module needs it,
|
||||||
|
not before. Modules that need none go ahead now.
|
||||||
|
|
||||||
|
## How every module moves
|
||||||
|
|
||||||
|
1. Written in the catalogue, with its tools and their tests, and checked by the controller's catalogue
|
||||||
|
check.
|
||||||
|
2. Merged, which builds it.
|
||||||
|
3. Assigned to **the first workstation, the proving machine**, and pushed. Its tools and files are
|
||||||
|
proven there.
|
||||||
|
4. Then assigned to every other machine it applies to, and pushed.
|
||||||
|
|
||||||
|
The operator delegated the go-ahead for each step on 2026-10-04 ("non-important decisions, easily
|
||||||
|
reversed"). Each step is reported.
|
||||||
|
|
||||||
|
**Adopting is also improving** (research 026, 027 overviews): every module lists what it fixes over
|
||||||
|
today, and leaves no predecessor copy of what it now owns.
|
||||||
|
|
||||||
|
## Phase 1 — every machine
|
||||||
|
|
||||||
|
In order:
|
||||||
|
|
||||||
|
| | module | owns | improves |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | `sudo` | the operator account's escalation, as a drop-in it owns | declares what three modules' tools assume and nothing stated |
|
||||||
|
| 2 | `localization` | locale, time zone, console keymap | one machine on another zone and keymap |
|
||||||
|
| 3 | `time-sync` | timesyncd and its servers | two different daemons across four machines |
|
||||||
|
| 4 | `pacman` | the package manager's configuration, mirrors and their refresh, cache cleaning | mirrors generated once and never again; caches never cleaned |
|
||||||
|
| 5 | `logrotate` | the timer and base configuration | rotation running on one machine of four |
|
||||||
|
| 6 | `avahi` | the daemon | on all four, owned by none |
|
||||||
|
| 7 | `systemd` | the service manager's tools (to-be 41 WP4) | built, assigned nowhere |
|
||||||
|
| 8 | `docker` | the runtime's packages, base configuration, group | four configurations, one owner on one machine |
|
||||||
|
| 9 | `ssh-client` | everything under `~/.ssh` (research 027/03) | a predecessor's entries winning over the mesh's; stale keys |
|
||||||
|
| 10 | scripts | the operator's own scripts, shared and per role (research 027/03) | under no version control, copied by hand |
|
||||||
|
| 11 | `kernel` | kernel, microcode, boot entries | two machines without microcode |
|
||||||
|
|
||||||
|
`systemd`, `pacman` and `docker` hold the three seats that apply resources
|
||||||
|
([ADR 0207](../../02-DECISIONS/0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md)):
|
||||||
|
every module that declares a service, a package or a container depends on them being held on its node.
|
||||||
|
They go on every machine before the rest, and once they have, an unmet dependency is refused rather
|
||||||
|
than reported.
|
||||||
|
|
||||||
|
`kernel` is last because a mistake in it costs a boot. `docker` stays a module without the runtime seat
|
||||||
|
until ADRs 0165 and 0166 are accepted.
|
||||||
|
|
||||||
|
## Phase 2 — both workstations
|
||||||
|
|
||||||
|
In order:
|
||||||
|
|
||||||
|
1. `fonts`;
|
||||||
|
2. `xorg` with autorandr;
|
||||||
|
3. `lemurs`;
|
||||||
|
4. `i3`;
|
||||||
|
5. `xterm`;
|
||||||
|
6. the theme module;
|
||||||
|
7. `picom`, `rofi`, `dmenu`, `dunst`, the lock module, `xclip`, the clipboard manager, `feh` and
|
||||||
|
`i3status-rust`;
|
||||||
|
8. `gnome-keyring`;
|
||||||
|
9. `docker-compose`, `snapd`, `flatpak`, `cups`, `bluetooth`.
|
||||||
|
|
||||||
|
The seats, gating and session-start questions of research 026 §2–§5 are recorded when `xorg` and `i3`
|
||||||
|
need them.
|
||||||
|
|
||||||
|
## Phase 3 — one machine model
|
||||||
|
|
||||||
|
The laptop's hardware module (vendor daemon, GPU mode, charge limit, logind, brightness and vendor keys)
|
||||||
|
and `memory-pressure` (research 027/03).
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
Each module's own tests and the catalogue check, at merge. On the proving machine, each tool answered
|
||||||
|
through the mesh and each owned file checked in place, before any other machine is assigned. This
|
||||||
|
document's tables are updated as each module lands.
|
||||||
@@ -43,6 +43,8 @@ document is written and this one's status becomes `implemented`.
|
|||||||
| [`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) |
|
| [`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) |
|
| [`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) |
|
||||||
|
| [`41-the-shell-and-the-accounts-environment.md`](41-the-shell-and-the-accounts-environment.md) | **In progress.** The shell and the account's environment as modules: an environment module every module contributes variables and `PATH` entries to, shell code contributed to the login shell in named slots, the prompt and plugins as modules, the host giving a login back, and the service manager's module finished | [ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md), [ADR 0204](../../02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md), [ADR 0205](../../02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md) |
|
||||||
|
| [`42-the-machines-modules-in-order.md`](42-the-machines-modules-in-order.md) | **In progress.** The order the machines' modules of research 026 and 027 are built and rolled out: every machine's first (sudo, localization, time sync, pacman, logrotate, avahi, systemd, docker, `~/.ssh`, scripts, kernel), then both workstations', then one machine model's; each proven on one workstation before the rest | [ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md), [ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md), [ADR 0205](../../02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md) |
|
||||||
|
|
||||||
## Not yet written
|
## Not yet written
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-09-30
|
opened: 2026-09-30
|
||||||
located-in: [mesh-host internal/apply (no removal for an archive)]
|
located-in: [mesh-host internal/apply (no removal for an archive)]
|
||||||
fixed-by:
|
fixed-by:
|
||||||
|
- mesh-host#90
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -54,3 +55,15 @@ argue for elsewhere: the mesh gives back what it found.
|
|||||||
A module with an archive is assigned, pushed, unassigned and pushed again; what the archive put on the
|
A module with an archive is assigned, pushed, unassigned and pushed again; what the archive put on the
|
||||||
machine is gone, anything that was in the directory beforehand is still there, and the apply that
|
machine is gone, anything that was in the directory beforehand is still there, and the apply that
|
||||||
removed it applied everything else in the same declaration.
|
removed it applied everything else in the same declaration.
|
||||||
|
|
||||||
|
## Resolved
|
||||||
|
|
||||||
|
*2026-10-04.* Hit again live the same day: a race between two pushes delivered a declaration without
|
||||||
|
a just-assigned module, and removing its tools bundle stopped a workstation's apply until the next push.
|
||||||
|
mesh-host#90 records what an archive unpacked: its files, the directories it made, whether the host
|
||||||
|
made the target and its parents. Undeclaring removes exactly those, never a file it did not place and
|
||||||
|
never a directory that was there before, and a failed removal is reported, never fatal.
|
||||||
|
|
||||||
|
An archive recorded before the change learns its list from its own bytes on the next apply while it is
|
||||||
|
still declared. One already orphaned is left in place, said and forgotten. Applying an archive no longer
|
||||||
|
deletes files the mesh did not place in its target directory (ADR 0030).
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
status: located
|
||||||
|
opened: 2026-10-04
|
||||||
|
located-in:
|
||||||
|
- mesh-host
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 228 — A login the mesh set is never given back, and undeclaring one stops the node applying
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
2026-10-04. Before the shell module of to-be 38 WP5 was assigned anywhere, a review traced what the
|
||||||
|
host does with the `user` shape the module declares (the operator account, with the login shell zsh)
|
||||||
|
on three events: first assign, a later push, and unassign.
|
||||||
|
|
||||||
|
1. **Undeclaring a `user` stops the node applying anything, for good.**
|
||||||
|
- The host's removal has no case for a `user`, so the orphaned record fails with "no way to remove".
|
||||||
|
- Orphans are removed before the declaration's first resource, and that failure aborts the apply.
|
||||||
|
- The record stays in the host's store, so every later apply fails the same way.
|
||||||
|
|
||||||
|
This was reproduced in a throwaway test against the host's code: the apply produced no outcomes,
|
||||||
|
and an unrelated file in the same declaration was not written. Renaming the resource's id has the
|
||||||
|
same effect. A showcase module carries a `user` today and is exposed to it too.
|
||||||
|
2. **The shell the account had is never recorded.** [ADR 0176](../../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)
|
||||||
|
§2 says the host "gives back [the login shell] when the holding moves". The host keeps nothing to
|
||||||
|
give back.
|
||||||
|
3. **A login shell is set whether or not it exists.** A failed package install does not stop the
|
||||||
|
resources after it. `usermod --shell` on the distribution only warns about a missing or
|
||||||
|
non-executable shell, and succeeds. The host's read-back compares the user database's string,
|
||||||
|
which matches. So an account can be pointed at a shell that is not there, and console, ssh and
|
||||||
|
display-manager logins then fail. No machine hit this, because zsh was already installed on all
|
||||||
|
four.
|
||||||
|
|
||||||
|
## Why it matters beyond this instance
|
||||||
|
|
||||||
|
Unassigning any module with a login in it, the case the mesh promises is ordinary, wedges the
|
||||||
|
machine's applies until a person edits the host's store. It is the same class of failure as an
|
||||||
|
earlier archive that could not be removed: a shape the host can create and cannot take away.
|
||||||
|
|
||||||
|
## Located
|
||||||
|
|
||||||
|
mesh-host, `internal/apply`: `remove()` has no `user` case, and `applyUser` neither records the shell
|
||||||
|
it replaced nor checks the shell it sets. The fix is set out in
|
||||||
|
[to-be 41](../../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md) WP1:
|
||||||
|
|
||||||
|
- a removal that never deletes an account;
|
||||||
|
- the login shell given back, if it is still the one the mesh set and the recorded one still exists;
|
||||||
|
- a shell refused before it is set unless it is executable and listed among the machine's shells
|
||||||
|
(a shell that refuses logins need only be executable, since the distribution does not list it and
|
||||||
|
the controller's own account uses one).
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-10-04
|
||||||
|
located-in: []
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 229 — A rollout cannot be followed through the mesh's tools, so an agent goes round them
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
2026-10-04, rolling out to-be 41. An agent drove the rollout through the mesh's MCP tools: the
|
||||||
|
controller seat's `status`, `plans`, `command`, and the forge's merge. Four times it left those tools
|
||||||
|
and posted JSON-RPC by hand to the node console's HTTP endpoint with `curl`:
|
||||||
|
|
||||||
|
1. **To wait for a plan.** `plans` answers once, with prose. Nothing waits for a plan to reach a tier,
|
||||||
|
finish or fail. An agent's tools cannot be called from a shell loop, so the only way to be told
|
||||||
|
when a plan moved was a background `curl` loop polling the console every twenty seconds and
|
||||||
|
matching the plan's line with `grep`.
|
||||||
|
2. **To read `status`.** `status` answers a paragraph of prose (the bus's user list), then a JSON
|
||||||
|
document, both inside one string. Picking out `behind`, `waiting` and `reported` took a script
|
||||||
|
that cut the string at the first brace and parsed the rest.
|
||||||
|
3. **To read one module out of `module list`,** whose output was too long to read whole for one line.
|
||||||
|
4. **To call a tool that arrived after the agent's session began.** The modules rolled out in that same
|
||||||
|
session added `node-login-shell.execute` and `zsh.zsh_config` to one machine. The agent's MCP
|
||||||
|
connection had been opened before the console moved to discovery ([ADR 0195](../../02-DECISIONS/0195-the-meshs-tools-are-found-by-address-not-announced-whole.md)).
|
||||||
|
It still held the flat catalogue the console announced then, which lacks both the new verbs and the five discovery tools (`mesh_call` among
|
||||||
|
them) the console announces now. Clearing a session does not reconnect its MCP servers, and the
|
||||||
|
console never sends a list-changed notice, so nothing told the client its list was stale. The agent
|
||||||
|
posted `mesh_machine` and `mesh_call` by hand. Reconnecting the server would have given it the
|
||||||
|
discovery tools, which reach any tool by address the moment it exists.
|
||||||
|
|
||||||
|
The calls were authorised, because the console is the operator's own surface. But each is a raw call
|
||||||
|
the mesh's tools were meant to make unnecessary ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
|
||||||
|
puts the controller's verbs behind the seat). Each is also a script that breaks silently when a
|
||||||
|
sentence in the prose changes.
|
||||||
|
|
||||||
|
## Why it matters beyond this instance
|
||||||
|
|
||||||
|
Every rollout an agent drives has the same shape: merge, wait for a plan, push, wait for reports,
|
||||||
|
check `status`. When the tools answer only once and only in prose, every agent writes its own poller
|
||||||
|
and its own parser. Those are invisible to review, different each time, and wrong the first time the
|
||||||
|
wording moves. An agent that cannot wait also tends to act early, which is the opposite of what a
|
||||||
|
rollout needs.
|
||||||
|
|
||||||
|
## What a fix has to settle
|
||||||
|
|
||||||
|
- A way to **wait** on the mesh's own progress. For example, `plans` and `status` could take a plan
|
||||||
|
or node and a bound, and answer when it moves or the bound passes. Or a verb could follow one plan
|
||||||
|
to its end.
|
||||||
|
- **Structured answers** from the controller's verbs, with the prose as a field beside the data, not
|
||||||
|
around it.
|
||||||
|
- Whether `command`'s generic answer should take a filter, or whether the verbs it is used for most
|
||||||
|
(`module list`, `node show`) deserve verbs of their own.
|
||||||
|
- **A client is told when the console's own surface changes.** The console announces `listChanged`
|
||||||
|
and sends the notice when what it lists changes, for example after an upgrade that changes its
|
||||||
|
tools. A long-running session then never keeps a list the console no longer serves. Discovery
|
||||||
|
already makes every module's tools reachable without the list changing.
|
||||||
|
|
||||||
|
How each is checked belongs to the record that settles it.
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-10-04
|
||||||
|
located-in:
|
||||||
|
- mesh-host
|
||||||
|
- mesh-controller
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 230 — A host that hands over to a newer one loses its report, and a plan waits for it for ever without saying so
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
2026-10-04, rolling out to-be 41. A new host build and a new controller were merged together. The
|
||||||
|
controller's plan built build-agent and sent every machine a fresh declaration, which also delivered
|
||||||
|
the new host. Three of the four machines logged, within the same second:
|
||||||
|
|
||||||
|
```
|
||||||
|
host 4bd7df099757 is delivered; standing aside so the launcher runs it
|
||||||
|
applied 546 resource(s)
|
||||||
|
applied, and could not tell the mesh: reporting: context canceled
|
||||||
|
nox-mesh-host-launch: running /usr/lib/nox-mesh-host/versions/4bd7df099757/nox-mesh-host
|
||||||
|
```
|
||||||
|
|
||||||
|
The new host came up and waited for its next declaration. The mesh never heard that the old one had
|
||||||
|
applied.
|
||||||
|
|
||||||
|
The plan then sat at "tier 1 built; waiting for build-agent on [three machines] to be applied", and
|
||||||
|
everything the mesh said about it read as healthy:
|
||||||
|
|
||||||
|
- `status` showed it as `rolling` with `"late": false`;
|
||||||
|
- `plans` printed "for 0s" on every look, so the wait never appeared to grow;
|
||||||
|
- the three machines' reports showed `current: false`, which reads like a machine that is merely slow.
|
||||||
|
|
||||||
|
Nothing logged, alerted or counted the wait. It was found because a person asked twice for the plan's
|
||||||
|
state, and the cause was found by reading a machine's own journal. A push to each of the three machines
|
||||||
|
released it: each new host applied and reported, and the plan moved on.
|
||||||
|
|
||||||
|
The same day, a second way to lose a report showed up. Assigning modules with tools to a workstation
|
||||||
|
changed the bus's user list, which the control machine's declaration carries. Applying it replaced the
|
||||||
|
bus's container, which cut every machine off for about fifteen seconds. The control machine itself
|
||||||
|
then logged `applied, and could not tell the mesh: reporting: nats: connection closed`. The report was
|
||||||
|
lost because the bus restarted under the apply that restarted it.
|
||||||
|
|
||||||
|
## Why it matters beyond this instance
|
||||||
|
|
||||||
|
**Every genuine host upgrade loses one report.**
|
||||||
|
[Issue 163](../163-a-delivered-host-stood-aside-on-every-push-and-reported-nothing/00-report.md) fixed
|
||||||
|
the host that stood aside on every push for the version it already ran. It named the mechanism, that
|
||||||
|
standing aside cancels the context the report is published with. That fix made standing aside
|
||||||
|
happen only for a real new version, but left the mechanism in place. So whenever a host build reaches
|
||||||
|
a machine, that apply's report is lost.
|
||||||
|
|
||||||
|
**And the mesh cannot tell a stuck wait from a slow one.** A plan that waits on a report that will
|
||||||
|
never come waits for ever, and nothing about it changes:
|
||||||
|
|
||||||
|
- its age does not grow ("for 0s");
|
||||||
|
- `late` stays false;
|
||||||
|
- nothing logs, emits an event or alerts.
|
||||||
|
|
||||||
|
This is [issue 187](../187-the-mesh-tells-nobody-when-it-stops-working/00-report.md)'s class of fault
|
||||||
|
again, *the mesh tells nobody when it stops working*, now in the rollout machinery that every merge
|
||||||
|
goes through. The operator's rule from issue 163 applies: if an answer has not come in the time an
|
||||||
|
answer takes, something is wrong, and the mesh must say so itself.
|
||||||
|
|
||||||
|
## What a fix has to settle
|
||||||
|
|
||||||
|
1. **A report survives whatever its own apply restarts.** The host publishes its report and has it
|
||||||
|
acknowledged before it stands aside. It retries a report the bus dropped once the link is back.
|
||||||
|
Failing that, the new host should report the declaration it took over,
|
||||||
|
naming the apply its predecessor finished. A lost report must be impossible, not merely unlikely.
|
||||||
|
2. **A plan's wait has an age and a bound.**
|
||||||
|
- "for 0s" must be the real time since the wait began.
|
||||||
|
- A wait past a bound, set by how long an apply takes rather than by a guess, makes the plan
|
||||||
|
`late`.
|
||||||
|
3. **Late is said where people and agents look.**
|
||||||
|
- It is said in `status` and in `plans`.
|
||||||
|
- It is logged as a warning by the controller.
|
||||||
|
- It is emitted as an event under the controller seat, so something can alert on it.
|
||||||
|
4. **A plan waiting on a machine the mesh has stopped hearing from** says that, by name, instead of
|
||||||
|
waiting. The machine's heartbeat already tells the controller it is alive. A live machine with an
|
||||||
|
unacknowledged declaration is the stuck case itself.
|
||||||
|
|
||||||
|
How each is checked belongs to the fix. For the host: a delivered upgrade, applied, is reported. For
|
||||||
|
the controller: a plan whose machine never reports turns `late` within its bound, and says so in
|
||||||
|
`status`, the log and an event.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-10-04
|
||||||
|
located-in:
|
||||||
|
- mesh-controller
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 231 — A misspelled placeholder is written out as text
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
2026-10-04, while studying the predecessor's failures. A manifest was checked whose one file holds four
|
||||||
|
placeholders: `${shel:zsh:first}` (a misspelled namespace), `${setting:Undeclared}` (a setting the module
|
||||||
|
does not declare), `${machnie:address}` (a misspelled namespace) and `${XDG_CACHE_HOME:-x}` (shell
|
||||||
|
syntax, which must pass through). The catalogue check, which runs the same functions registration does,
|
||||||
|
answered `ok`.
|
||||||
|
|
||||||
|
The controller fills each namespace it knows with its own pattern (`machine`, `setting`, `bound`, `dir`,
|
||||||
|
`port`, `secret`, `environment`, `shell`, …). A word in that shape that no pass consumes is left in the
|
||||||
|
file as it was written. A misspelling therefore reaches a machine as literal text, in a configuration
|
||||||
|
file that then reads it as a value.
|
||||||
|
|
||||||
|
## Why it matters beyond this instance
|
||||||
|
|
||||||
|
This is exactly the predecessor's failure: an unresolved template variable in a destination path
|
||||||
|
installed green, and a literal `${...}` path stood under `/etc/ssl` until somebody looked. The mesh's
|
||||||
|
namespaced placeholders were meant to end it ([ADR 0164](../../02-DECISIONS/0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md)),
|
||||||
|
and they do for every name spelled right.
|
||||||
|
|
||||||
|
## What a fix has to settle
|
||||||
|
|
||||||
|
After every pass, a final sweep refuses any remaining `${<word>:` whose word is a lower-case
|
||||||
|
namespace-shaped token, naming the module, the field and the token, at the catalogue check and at
|
||||||
|
composition. Shell syntax (`${NAME:-…}`, `${(%):-…}`, `${1:-.}`) is not namespace-shaped and passes. So
|
||||||
|
does contributed shell code, which no pass reads (ADR 0204). A setting a module uses but does not
|
||||||
|
declare is refused the same way.
|
||||||
|
|
||||||
|
How it is checked: the controller's test with the four placeholders above, three refused by name and
|
||||||
|
one passed through.
|
||||||
Reference in New Issue
Block a user