Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5440a144a0 |
@@ -2,7 +2,7 @@
|
||||
status: graduated
|
||||
initiated: 2026-10-04
|
||||
touches: [the bus, what a module declares, the tool runtime, the SDK, the bus grants, 03-DESIGN/01-to-be/25-the-bus-on-nats.md, 03-DESIGN/01-to-be/32-what-a-module-declares.md]
|
||||
became: [02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md, 03-DESIGN/01-to-be/32-what-a-module-declares.md, 03-DESIGN/01-to-be/25-the-bus-on-nats.md]
|
||||
became: [02-DECISIONS/0202-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md, 03-DESIGN/01-to-be/32-what-a-module-declares.md, 03-DESIGN/01-to-be/25-the-bus-on-nats.md]
|
||||
---
|
||||
|
||||
# 024 — State a module keeps on the bus
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
---
|
||||
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
@@ -1,103 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,197 +0,0 @@
|
||||
# 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.
|
||||
-2
@@ -9,8 +9,6 @@ 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
|
||||
|
||||
> **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
|
||||
|
||||
[ADR 0011](0011-managed-files-are-generated-never-edited.md) says a managed file is derived and an
|
||||
|
||||
@@ -9,8 +9,6 @@ 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
|
||||
|
||||
> **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
|
||||
|
||||
[ADR 0040](0040-what-a-module-is.md) names the shell as its example of a *shared* seat: bash, zsh
|
||||
|
||||
+4
-7
@@ -7,14 +7,11 @@ reconstructed: false
|
||||
extends: 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
|
||||
---
|
||||
|
||||
# 202. A provider declares what it derives for each consumer, and the mesh tells both ends
|
||||
# 201. A provider declares what it derives for each consumer, and the mesh tells both ends
|
||||
|
||||
> **Written as 0188 on 2026-10-02, renumbered to 0201, and to 0202 on 2026-10-04.** Twice, for the
|
||||
> same reason twice: the bundles refactor took 0188 while this waited in a pull request, and the
|
||||
> key-value-buckets record took 0201 while this waited again. Both times the number was free when
|
||||
> it was chosen and taken by the time this merged. Only the number moved; the decision is the one
|
||||
> taken on the 2nd. The check that refuses two records sharing a number is what caught it, both
|
||||
> times — a number is how a record is cited, and three repositories cite this one.
|
||||
> Written as 0188 on 2026-10-02 and renumbered to 0201 on 2026-10-04: the record of the bundles
|
||||
> refactor took 0188 on main while this one waited in a pull request, and the mesh's own code now
|
||||
> cites that one. Only the number moved; the decision is the one taken on the 2nd.
|
||||
|
||||
## Context
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ reconstructed: false
|
||||
extends: 02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md
|
||||
---
|
||||
|
||||
# 201. A module keeps its current state in key-value buckets it declares, and reaches them through the runtime
|
||||
# 202. A module keeps its current state in key-value buckets it declares, and reaches them through the runtime
|
||||
|
||||
## Context
|
||||
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
---
|
||||
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
@@ -1,131 +0,0 @@
|
||||
---
|
||||
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
@@ -1,69 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -190,7 +190,7 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0187** — [A dead tracker is not the machine's failure](0187-a-dead-tracker-is-not-the-machines-failure.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)
|
||||
- **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)
|
||||
- **0201** — [A provider declares what it derives for each consumer, and the mesh tells both ends](0201-a-provider-declares-what-it-derives-for-each-consumer.md)
|
||||
|
||||
### Its tiers, from the bottom up
|
||||
|
||||
@@ -300,10 +300,7 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0195** — [The mesh's tools are found by address, not announced whole](0195-the-meshs-tools-are-found-by-address-not-announced-whole.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)
|
||||
- **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)
|
||||
- **0202** — [A module keeps its current state in key-value buckets it declares, and reaches them through the runtime](0202-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)
|
||||
|
||||
### How it is built
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ code:
|
||||
- mesh-tools src/broker-amqp.ts (to be replaced)
|
||||
- mesh-catalog modules/nats (to be written)
|
||||
- mesh-sdk src (the protocol's NATS binding, step 3)
|
||||
- mesh-tools node-tools/internal/bus (a module's state, ADR 0201)
|
||||
- mesh-tools node-tools/internal/bus (a module's state, ADR 0202)
|
||||
updated: 2026-10-04
|
||||
decisions:
|
||||
- 02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md
|
||||
- 02-DECISIONS/0202-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md
|
||||
- 02-DECISIONS/0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md
|
||||
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||
@@ -60,7 +60,7 @@ property of the mesh's architecture that happens to be expressed in subjects.
|
||||
|
||||
And more of the mesh lands here as it is built: conditions and observed state in key-value
|
||||
buckets that anything may watch — the first of them a module's own declared state, *2026-10-04*
|
||||
([ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)) — the server's own advisories becoming observations like any other
|
||||
([ADR 0202](../../02-DECISIONS/0202-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)) — the server's own advisories becoming observations like any other
|
||||
([research 017](../../01-RESEARCH/017-a-mesh-that-heals-itself/00-overview.md)), and a person's
|
||||
client speaking the bus directly rather than through a surface built over it (§7). None of that
|
||||
is a message being moved; all of it is the bus being the mesh's centre.
|
||||
@@ -91,7 +91,7 @@ mesh.assignment.<node>.<module> an assignment's membership (JetStream: ASSIG
|
||||
$KV.<module>_<name>.<key> a module's state (JetStream: a key-value bucket per declared name)
|
||||
```
|
||||
|
||||
**Added 2026-10-04** ([ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)):
|
||||
**Added 2026-10-04** ([ADR 0202](../../02-DECISIONS/0202-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)):
|
||||
the last row is outside `mesh.` on purpose. A key-value bucket is NATS's own construct and lives
|
||||
under NATS's own prefix, which is what lets the server's key-value layer — direct reads, rollups,
|
||||
delete markers, watches — do the work instead of the mesh writing it again. The bucket is named for
|
||||
@@ -167,7 +167,7 @@ Core NATS is at-most-once. Everything the mesh must not lose lives in a JetStrea
|
||||
| CONTROL | `mesh.control.>` except `alive` (a build's outcome moved to its seat, ADR 0121) | work queue, one consumer (the controller), explicit ack | the store-window guarantee ([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)): the controller `nak`s with a delay while its store is away and the message is redelivered; nothing is dropped |
|
||||
| NODES | `mesh.node.>` | last per subject | one declaration per node, always the newest |
|
||||
| EVENTS | `mesh.mod.*.event.>` and `mesh.seat.*.event.>` | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream). *2026-10-01:* a build's whole log is here too, as the build-machine seat's `log.<build id>` events ([ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)) — one subject per build, a week of retention, read back by `builds --log <id>` with a consumer that is gone when the reading is done |
|
||||
| `KV_<module>_<name>` | `$KV.<module>_<name>.>` | the newest value per key — as many past values as the owner declared — no age unless the owner declared one; a value at most 256 KiB, a bucket at most 64 MiB | a module's state (ADR 0201): one per name in a manifest's `state`, created from the catalogue on every raise, so it exists before its owner runs anywhere; kept when the module is unassigned, because what it holds is data |
|
||||
| `KV_<module>_<name>` | `$KV.<module>_<name>.>` | the newest value per key — as many past values as the owner declared — no age unless the owner declared one; a value at most 256 KiB, a bucket at most 64 MiB | a module's state (ADR 0202): one per name in a manifest's `state`, created from the catalogue on every raise, so it exists before its owner runs anywhere; kept when the module is unassigned, because what it holds is data |
|
||||
|
||||
Tool calls and heartbeats stay on core NATS: a lost heartbeat is the next heartbeat; a lost tool
|
||||
call is a timeout the caller already handles.
|
||||
@@ -247,7 +247,7 @@ expresses this exactly, per subject, and better than a vhost could:
|
||||
permissions for each consumed event's subject, its tool subjects, and that same inbox prefix.
|
||||
Nothing else. A module that tries to publish outside its emits is refused by the server, not by
|
||||
convention.
|
||||
- **A module's state** (ADR 0201), for whichever principal carries the module — today the machine's
|
||||
- **A module's state** (ADR 0202), for whichever principal carries the module — today the machine's
|
||||
runtime, whose grant is the union of its modules': binding to the bucket, reading a key directly,
|
||||
and an ordered consumer for listing and watching, created and deleted on the bucket's own stream
|
||||
and nothing else's; and, for the owner's instances only, publishing under the bucket's own
|
||||
|
||||
@@ -14,7 +14,7 @@ decisions:
|
||||
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
|
||||
- 02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md
|
||||
- 02-DECISIONS/0038-the-mesh-assigns-the-port.md
|
||||
- 02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md
|
||||
- 02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md
|
||||
---
|
||||
|
||||
# 27 — A module requires, the mesh resolves
|
||||
@@ -208,7 +208,7 @@ the placeholder allows: the definition says which values reach which requirement
|
||||
does. *How it is checked:* the unit tests named in issue 173, and the plan comparison that closed it.
|
||||
|
||||
*A provider says once what it derives for each consumer (2026-10-02,
|
||||
[ADR 0202](../../02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md),
|
||||
[ADR 0201](../../02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md),
|
||||
[issue 124](../../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md)):*
|
||||
where a provider **names the resource** it gives each consumer — a bucket, a database, a vhost — the
|
||||
name is derived per consumer, and a literal `serves` block could not carry it. A served value may
|
||||
@@ -221,7 +221,7 @@ its binding's served facts and as `${bound:<provision>:<key>}` in any file it wr
|
||||
as `derived` on that consumer's entry in its contributions file, so its provisioner is told the name
|
||||
rather than recomputing it. A consumer that writes the derived value into its own definition instead
|
||||
of asking for it is refused, naming the placeholder to use. *How it is checked:* the unit tests in
|
||||
ADR 0202's "how this is checked", each run against the unchanged controller first.
|
||||
ADR 0201's "how this is checked", each run against the unchanged controller first.
|
||||
|
||||
## How a definition reads what was resolved
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ code:
|
||||
- mesh-host internal/apply/apply.go
|
||||
- mesh-tools src/main.ts
|
||||
- mesh-catalog modules/mesh-catalog
|
||||
- mesh-tools node-tools/internal/runtime (a module's state, ADR 0201)
|
||||
- mesh-tools node-tools/internal/runtime (a module's state, ADR 0202)
|
||||
updated: 2026-10-04
|
||||
decisions:
|
||||
- 02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md
|
||||
- 02-DECISIONS/0202-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md
|
||||
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||
@@ -241,7 +241,7 @@ That is the wire-level answer to
|
||||
[issue 107](../../04-ISSUES/107-a-declaration-carries-no-order/00-report.md).
|
||||
|
||||
**A module declares state too.** *Added 2026-10-04,
|
||||
[ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md).*
|
||||
[ADR 0202](../../02-DECISIONS/0202-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md).*
|
||||
State was the mesh's alone, and modules had the same need with nowhere to put it: an MCP server
|
||||
registered for every machine, sent as an event, never reached a machine assigned afterwards — its
|
||||
consumer did not exist yet when the event passed — and a licence binding sent as events replays a
|
||||
@@ -484,7 +484,7 @@ sealing key leaks, that stream is an archive rather than a moment. So:
|
||||
it is worst.
|
||||
|
||||
**A key-value bucket is a stream, so the same holds for it.** *Added 2026-10-04,
|
||||
[ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md).*
|
||||
[ADR 0202](../../02-DECISIONS/0202-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md).*
|
||||
No secret is put in a module's state, sealed or not: state is exactly what a machine joining a year
|
||||
later reads in full. A value that needs a secret names it, and the secret travels on request/reply.
|
||||
Sealed values are plain text to anything inspecting them, so this is checked only partly — the
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-host, mesh-controller, mesh-tools, mesh-catalog]
|
||||
updated: 2026-10-04
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
|
||||
- 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md
|
||||
@@ -33,16 +33,12 @@ 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:
|
||||
|
||||
- a **package**, `zsh`;
|
||||
- **files under the home**, owned by the account: the mesh's block at the start of the shell's rc
|
||||
file with the module's default configuration and the slots other modules' code lands in, the
|
||||
operator's own lines kept after it, and `${setting:…}` placeholders for the few values a node
|
||||
varies; the account and its home are machine facts the controller resolves
|
||||
([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md),
|
||||
to-be 29 §2). 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;
|
||||
- **files under the home**, owned by the account: the shell's rc file with the module's default
|
||||
configuration, carrying a kept region for the operator's own lines, and `${setting:…}`
|
||||
placeholders for the few values a node varies; the account and its home are machine facts the
|
||||
controller resolves ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md),
|
||||
to-be 29 §2);
|
||||
- a **seat declaration**, `login-shell`, node-scoped, with its one verb; and a **claim** on it;
|
||||
- a **`user` shape** naming the shell, applied only where the module holds the seat;
|
||||
- a **tools bundle**, the artifact kind for interpreted code, with `execute` and the module's own
|
||||
`show-config`.
|
||||
@@ -84,8 +80,7 @@ root escalates itself.
|
||||
|
||||
## 4. The seats of the environment
|
||||
|
||||
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
|
||||
Decided now: **`login-shell`** (module-declared; zsh, fish, bash; verb `execute`) and
|
||||
**`node-service-manager`** (the mesh's own; systemd; verbs over units in both scopes). The rest
|
||||
are candidates from [research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md),
|
||||
one record each when its first holder is written: display server, display session, terminal
|
||||
|
||||
@@ -15,7 +15,6 @@ decisions:
|
||||
- 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/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
|
||||
@@ -331,13 +330,6 @@ tool of each.
|
||||
|
||||
## 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.*
|
||||
|
||||
**Order.** Assign `zsh` to one server; push; `login-shell.execute@<server> command="uptime"`
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -43,7 +43,6 @@ 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) |
|
||||
| [`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) |
|
||||
| [`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) |
|
||||
|
||||
## Not yet written
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
status: resolved
|
||||
opened: 2026-09-26
|
||||
located-in: [mesh-controller internal/catalogue, mesh-sdk src/provisioner, mesh-catalog modules/minio]
|
||||
fixed-by: 02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md
|
||||
fixed-by: 02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md
|
||||
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||
---
|
||||
|
||||
@@ -64,7 +64,7 @@ compares it to what the provider will actually create. The one wrong instance wa
|
||||
bucket in its own configuration against the one the provider would create is a check that could
|
||||
exist today, for any interface, without the mechanism above.
|
||||
|
||||
## Answered, 2026-10-02 — [ADR 0202](../../02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md)
|
||||
## Answered, 2026-10-02 — [ADR 0201](../../02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md)
|
||||
|
||||
The channel is the provider's own `serves` block, which may now name the consumer the mesh is
|
||||
serving: `${consumer:as}` and `${consumer:as:dns}`. The mesh fills it once, where it knows who the
|
||||
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-10-04
|
||||
located-in: [mesh-host internal/apply, mesh-controller internal/catalogue]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 225 — A provisioner cannot read the grant secrets since its code left the container, and every consumer of it is unserved
|
||||
|
||||
## What was observed
|
||||
|
||||
On the control machine, 2026-10-04, found while looking at why one app was restarting:
|
||||
|
||||
```
|
||||
[mongodb] [provisioner:mongodb-database] mesh_novox_photos: secret not readable yet
|
||||
(/var/lib/mongodb/grants/novox.photos.secret):
|
||||
Error: EACCES: permission denied, open '/var/lib/mongodb/grants/novox.photos.secret'
|
||||
```
|
||||
|
||||
**4330 times, every five seconds, since 01:30:20.** The consequence is not a log line: the
|
||||
provisioner never reads the password, so it never creates the user, so the consumer never
|
||||
connects —
|
||||
|
||||
```
|
||||
UserNotFound: Could not find user "mesh_novox_photos" for db "admin"
|
||||
```
|
||||
|
||||
— and the app crash-loops. Two consumers on this machine are in that state.
|
||||
|
||||
## Why
|
||||
|
||||
The grant secrets are what the mesh seals for each consumer and the host unseals beside the
|
||||
provider's contributions file. They are written `-rw------- root root`, which was right while a
|
||||
module's own code ran in a container as root.
|
||||
|
||||
[ADR 0198](../../02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)
|
||||
moved a module's long-running code out of its container and under the node's runtime, which runs
|
||||
as the operator's account. The provisioner is now that account; the secret is still root's. The
|
||||
timestamps say it exactly: the files are dated 2026-09-26, the first refusal is 01:30:20 on the
|
||||
day the runtime rolled.
|
||||
|
||||
**Nothing reports it.** The machine applies cleanly and reads as current; the provisioner says
|
||||
`secret not readable yet`, whose wording is for a real and ordinary race on the first pass — the
|
||||
host has not written the file yet — and which is indistinguishable, in the log, from a permanent
|
||||
refusal. Four thousand occurrences of a message that means "wait a moment" is the shape to
|
||||
recognise.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
This is every provider that provisions. The grant secret is the one file the sdk's harness reads
|
||||
for every consumer, so a provider that cannot read it serves nobody — and says so only in a line
|
||||
that reads like patience.
|
||||
|
||||
It is also the general question the runtime move leaves: **what the mesh seals for a module is
|
||||
owned for the shape that module's code used to have.** Each module whose code moved is a module
|
||||
whose files may now be unreadable to it, and ownership is the mesh's to state, not the module's
|
||||
to work around.
|
||||
|
||||
## What this is not
|
||||
|
||||
Not caused by [ADR 0202](../../02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md)
|
||||
or [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md), which landed two
|
||||
to three hours after the first refusal. Those rebuilt the two affected consumers, which recreated
|
||||
their containers and made a silent fault visible as a restarting one. The dates are above.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Who owns a grant secret now — the module's account, as `secrets-owner` already says for a
|
||||
module's own secrets? Then the host writes it so, and this is a one-line statement in the
|
||||
declaration rather than a convention.
|
||||
- Should `secret not readable yet` stop saying "yet" after the first few passes? A message that
|
||||
is right once and wrong four thousand times is a message that hides its own meaning.
|
||||
- Which other modules' files did the runtime move leave behind? The sweep is the same question
|
||||
for every path the mesh writes for a module: directories, bundles, received files.
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-10-04
|
||||
located-in: [mesh-controller cmd/mesh-controller/collect.go, mesh-controller internal/inventory/collection.go]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 226 — The store's sweep stops at the first reference recorded with an address, so it collects nothing at all
|
||||
|
||||
## What was observed
|
||||
|
||||
The first live run of [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md)'s
|
||||
sweep, 2026-10-04, printed on every build:
|
||||
|
||||
```
|
||||
the artifact store kept 127.0.0.1:5100/mesh-tools/build@sha256:0de48cd3…, so nothing more was
|
||||
asked of it: 127.0.0.1:5100/mesh-tools/build@sha256:0de48cd3… is not a reference into the
|
||||
mesh's artifact store
|
||||
1681 more to collect; the next build asks again
|
||||
```
|
||||
|
||||
Nothing is collected, and nothing ever will be. The store holds 1681 artifacts the mesh no longer
|
||||
keeps and the feature that exists to remove them is inert.
|
||||
|
||||
## Why
|
||||
|
||||
Two correct decisions meeting badly.
|
||||
|
||||
**A reference recorded before references were kept without an address** is
|
||||
`127.0.0.1:5100/<path>@sha256:…` rather than `artifact-store://<path>@sha256:…`
|
||||
([04-ISSUES/102](../102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)).
|
||||
`LetGo` rightly refuses to compose a delete for a reference whose shape it does not recognise —
|
||||
that refusal is what keeps the sweep from reaching something that is not the mesh's.
|
||||
|
||||
**The sweep stops at the first refusal**, because "a store that refuses one refuses all of them"
|
||||
— deletion disabled, the store down, the network gone — and pushing through would mean a hundred
|
||||
identical failures in front of whoever was building something. That reasoning is right for the
|
||||
store refusing. It is wrong for *this* record being unreadable.
|
||||
|
||||
So one old record, early in the oldest-first order, halts the whole sweep for ever.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
**A guard that cannot tell "I will not ask about this" from "it would not answer" stops the wrong
|
||||
amount of work.** The two deserve opposite responses: skip one, abandon the other. Collapsing
|
||||
them into "an error" is how a bounded, cautious loop becomes a loop that does nothing — and it
|
||||
reports the right number while doing it, which is what made it look healthy.
|
||||
|
||||
## What a fix has to settle
|
||||
|
||||
- A reference the sweep cannot address is **skipped, and the sweep goes on** — it is a fact about
|
||||
that record, not about the store.
|
||||
- `Recorded()` already normalises the old form to the kept one, and is what the rest of the mesh
|
||||
uses for exactly these references. The sweep should normalise before asking rather than refuse.
|
||||
- Only a refusal *by the store* ends a sweep.
|
||||
- **How it is checked:** a sweep over records holding one address-recorded reference and one kept
|
||||
one collects the second; a sweep against a store that refuses stops at the first.
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-10-04
|
||||
located-in: [mesh-catalog modules/photos]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 227 — The photo app's admin client asks for port 80, which the reverse proxy holds, so it cannot start
|
||||
|
||||
## What was observed
|
||||
|
||||
Applying the control machine, 2026-10-04:
|
||||
|
||||
```
|
||||
applying "photos.admin-client": starting container photos-admin-client:
|
||||
failed to bind host port 0.0.0.0:80/tcp: address already in use
|
||||
```
|
||||
|
||||
Port 80 on that machine belongs to the reverse proxy (`mesh-route-proxy`, confirmed with `ss`),
|
||||
which is the whole arrangement: the proxy holds the public ports and every module is reached
|
||||
through it. A module that publishes 80 itself can never start beside it.
|
||||
|
||||
Everything else on the machine applied; this one resource fails every pass.
|
||||
|
||||
## How it surfaced
|
||||
|
||||
`photos` had been pinned at a commit from 2026-09-28 and was rebuilt to `main` on 2026-10-04 —
|
||||
forced by [ADR 0202](../../02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md)'s
|
||||
refusal of its transcribed bucket name. The admin client is one of the changes that came with the
|
||||
rest of `main`. The rebuild did not create the conflict; it delivered it.
|
||||
|
||||
**A module pinned months behind carries whatever its branch gained, all at once, the first time
|
||||
something makes it move.** That is the cost of a pin, and it is paid in full rather than
|
||||
gradually.
|
||||
|
||||
## What a fix has to settle
|
||||
|
||||
- Which port the admin client should ask for, or whether it should be reached through the proxy
|
||||
like everything else and publish nothing.
|
||||
- Whether a module declaring a port the machine's proxy already holds should be refused when it
|
||||
is composed, rather than failing on the machine every pass. The mesh assigns ports
|
||||
([ADR 0038](../../02-DECISIONS/0038-the-mesh-assigns-the-port.md)); a fixed 80 beside a proxy is
|
||||
a statement it could check.
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
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).
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
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
@@ -1,87 +0,0 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user