Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5440a144a0 |
@@ -2,7 +2,7 @@
|
|||||||
status: graduated
|
status: graduated
|
||||||
initiated: 2026-10-04
|
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]
|
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
|
# 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
|
# 174. A node varies a module through settings and kept regions, never through an edit
|
||||||
|
|
||||||
> **The mechanism changed — 2026-10-04, by [ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md).** Where this record calls a kept region *a marked block in which the operator's own lines are kept*, read the inverse, which is what the host built: the mesh's region is the marked block, and every line outside it is the operator's, kept byte for byte and given back when the module goes. The decision stands: a node varies a module by settings and by the operator's own lines, never by an edit.
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
[ADR 0011](0011-managed-files-are-generated-never-edited.md) says a managed file is derived and an
|
[ADR 0011](0011-managed-files-are-generated-never-edited.md) says a managed file is derived and an
|
||||||
|
|||||||
@@ -9,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
|
# 176. The login shell is a node seat held by one shell module, and `execute` is its contract
|
||||||
|
|
||||||
> **The mechanism changed — 2026-10-04, by [ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md).** The seat is no longer declared by the shell modules (§1). It is `node-login-shell`, in the mesh's own seat set, which a shell module claims. Its holder also places the shell code other modules contribute, and sources the account's environment ([ADR 0203](0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md)). What stands: one holder per node, the login shell set by the `user` shape and given back, `execute` as the contract, and any node may call it.
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
[ADR 0040](0040-what-a-module-is.md) names the shell as its example of a *shared* seat: bash, zsh
|
[ADR 0040](0040-what-a-module-is.md) names the shell as its example of a *shared* seat: bash, zsh
|
||||||
|
|||||||
+4
-7
@@ -7,14 +7,11 @@ reconstructed: false
|
|||||||
extends: 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
|
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
|
> Written as 0188 on 2026-10-02 and renumbered to 0201 on 2026-10-04: the record of the bundles
|
||||||
> same reason twice: the bundles refactor took 0188 while this waited in a pull request, and the
|
> refactor took 0188 on main while this one waited in a pull request, and the mesh's own code now
|
||||||
> key-value-buckets record took 0201 while this waited again. Both times the number was free when
|
> cites that one. Only the number moved; the decision is the one taken on the 2nd.
|
||||||
> 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.
|
|
||||||
|
|
||||||
## Context
|
## 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
|
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
|
## 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)
|
- **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)
|
- **0189** — [The store keeps what the records name, and a maintenance step holds its writers still](0189-the-store-keeps-what-the-records-name.md)
|
||||||
- **0190** — [A seat's work is shared by its holders, and building is the first such role](0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md)
|
- **0190** — [A seat's work is shared by its holders, and building is the first such role](0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md)
|
||||||
- **0202** — [A provider declares what it derives for each consumer, and the mesh tells both ends](0202-a-provider-declares-what-it-derives-for-each-consumer.md)
|
- **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
|
### 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)
|
- **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)
|
- **0197** — [Every tool announces itself on the bus, in the NATS services protocol](0197-every-tool-announces-itself-on-the-bus-in-the-nats-services-protocol.md)
|
||||||
- **0198** — [A module's long-running code is launched by the node's runtime, and reaches the bus through it](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)
|
- **0198** — [A module's long-running code is launched by the node's runtime, and reaches the bus through it](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)
|
||||||
- **0201** — [A module keeps its current state in key-value buckets it declares, and reaches them through the runtime](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)
|
- **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)
|
||||||
- **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)
|
|
||||||
|
|
||||||
### How it is built
|
### How it is built
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ code:
|
|||||||
- mesh-tools src/broker-amqp.ts (to be replaced)
|
- mesh-tools src/broker-amqp.ts (to be replaced)
|
||||||
- mesh-catalog modules/nats (to be written)
|
- mesh-catalog modules/nats (to be written)
|
||||||
- mesh-sdk src (the protocol's NATS binding, step 3)
|
- mesh-sdk src (the protocol's NATS binding, step 3)
|
||||||
- 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
|
updated: 2026-10-04
|
||||||
decisions:
|
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/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/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||||
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
- 02-DECISIONS/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
|
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*
|
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
|
([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
|
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.
|
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)
|
$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
|
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,
|
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
|
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 |
|
| CONTROL | `mesh.control.>` except `alive` (a build's outcome moved to its seat, ADR 0121) | work queue, one consumer (the controller), explicit ack | the store-window guarantee ([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)): the controller `nak`s with a delay while its store is away and the message is redelivered; nothing is dropped |
|
||||||
| NODES | `mesh.node.>` | last per subject | one declaration per node, always the newest |
|
| NODES | `mesh.node.>` | last per subject | one declaration per node, always the newest |
|
||||||
| EVENTS | `mesh.mod.*.event.>` 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 |
|
| 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
|
Tool calls and heartbeats stay on core NATS: a lost heartbeat is the next heartbeat; a lost tool
|
||||||
call is a timeout the caller already handles.
|
call is a timeout the caller already handles.
|
||||||
@@ -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.
|
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
|
Nothing else. A module that tries to publish outside its emits is refused by the server, not by
|
||||||
convention.
|
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,
|
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 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
|
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/0084-which-provider-serves-a-consumer.md
|
||||||
- 02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.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/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
|
# 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.
|
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,
|
*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)):*
|
[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
|
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
|
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
|
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
|
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
|
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
|
## How a definition reads what was resolved
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ code:
|
|||||||
- mesh-host internal/apply/apply.go
|
- mesh-host internal/apply/apply.go
|
||||||
- mesh-tools src/main.ts
|
- mesh-tools src/main.ts
|
||||||
- mesh-catalog modules/mesh-catalog
|
- mesh-catalog modules/mesh-catalog
|
||||||
- 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
|
updated: 2026-10-04
|
||||||
decisions:
|
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/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||||
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
||||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||||
@@ -241,7 +241,7 @@ That is the wire-level answer to
|
|||||||
[issue 107](../../04-ISSUES/107-a-declaration-carries-no-order/00-report.md).
|
[issue 107](../../04-ISSUES/107-a-declaration-carries-no-order/00-report.md).
|
||||||
|
|
||||||
**A module declares state too.** *Added 2026-10-04,
|
**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
|
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
|
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
|
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.
|
it is worst.
|
||||||
|
|
||||||
**A key-value bucket is a stream, so the same holds for it.** *Added 2026-10-04,
|
**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
|
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.
|
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
|
Sealed values are plain text to anything inspecting them, so this is checked only partly — the
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: in-progress
|
status: in-progress
|
||||||
code: [mesh-host, mesh-controller, mesh-tools, mesh-catalog]
|
code: [mesh-host, mesh-controller, mesh-tools, mesh-catalog]
|
||||||
updated: 2026-10-04
|
updated: 2026-10-02
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
|
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
|
||||||
- 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md
|
- 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md
|
||||||
@@ -33,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:
|
Worked on the first one, a shell. The `zsh` module declares:
|
||||||
|
|
||||||
- a **package**, `zsh`;
|
- a **package**, `zsh`;
|
||||||
- **files under the home**, owned by the account: the mesh's block at the start of the shell's rc
|
- **files under the home**, owned by the account: the shell's rc file with the module's default
|
||||||
file with the module's default configuration and the slots other modules' code lands in, the
|
configuration, carrying a kept region for the operator's own lines, and `${setting:…}`
|
||||||
operator's own lines kept after it, and `${setting:…}` placeholders for the few values a node
|
placeholders for the few values a node varies; the account and its home are machine facts the
|
||||||
varies; the account and its home are machine facts the controller resolves
|
controller resolves ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md),
|
||||||
([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md),
|
to-be 29 §2);
|
||||||
to-be 29 §2). Its environment is a contribution to the environment module, not lines of its own
|
- a **seat declaration**, `login-shell`, node-scoped, with its one verb; and a **claim** on it;
|
||||||
([ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md),
|
|
||||||
[ADR 0204](../../02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md),
|
|
||||||
[to-be 41](41-the-shell-and-the-accounts-environment.md));
|
|
||||||
- a **claim** on the mesh's node-scoped seat `node-login-shell`, with its one verb;
|
|
||||||
- a **`user` shape** naming the shell, applied only where the module holds the seat;
|
- a **`user` shape** naming the shell, applied only where the module holds the seat;
|
||||||
- a **tools bundle**, the artifact kind for interpreted code, with `execute` and the module's own
|
- a **tools bundle**, the artifact kind for interpreted code, with `execute` and the module's own
|
||||||
`show-config`.
|
`show-config`.
|
||||||
@@ -84,8 +80,7 @@ root escalates itself.
|
|||||||
|
|
||||||
## 4. The seats of the environment
|
## 4. The seats of the environment
|
||||||
|
|
||||||
Decided now: **`node-login-shell`** (the mesh's own, ADR 0204; zsh, fish, bash; verb `execute`),
|
Decided now: **`login-shell`** (module-declared; zsh, fish, bash; verb `execute`) and
|
||||||
**`node-environment`** (the mesh's own, ADR 0203; the environment module; no verbs) and
|
|
||||||
**`node-service-manager`** (the mesh's own; systemd; verbs over units in both scopes). The rest
|
**`node-service-manager`** (the mesh's own; systemd; verbs over units in both scopes). The rest
|
||||||
are candidates from [research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md),
|
are candidates from [research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md),
|
||||||
one record each when its first holder is written: display server, display session, terminal
|
one record each when its first holder is written: display server, display session, terminal
|
||||||
|
|||||||
@@ -15,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/0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md
|
||||||
- 02-DECISIONS/0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md
|
- 02-DECISIONS/0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md
|
||||||
- 02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md
|
- 02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md
|
||||||
- 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 38. Building the operator's machine
|
# 38. Building the operator's machine
|
||||||
@@ -331,13 +330,6 @@ tool of each.
|
|||||||
|
|
||||||
## WP5 — The shell, on a server first
|
## WP5 — The shell, on a server first
|
||||||
|
|
||||||
*Replaced on 2026-10-04 by [to-be 41](41-the-shell-and-the-accounts-environment.md).* A review before
|
|
||||||
assigning found that the shell module would duplicate every machine's existing startup file, drop
|
|
||||||
lines from it, leave the prompt uninstalled, and could not be unassigned
|
|
||||||
([issue 228](../../04-ISSUES/228-a-login-the-mesh-set-is-never-given-back/00-report.md)). The shell,
|
|
||||||
its environment, the modules that plug into it, and the host's fix are built and proven there. What
|
|
||||||
follows is the original plan, kept for the record.
|
|
||||||
|
|
||||||
*mesh-catalog #224, already written. Half a day to assign and prove.*
|
*mesh-catalog #224, already written. Half a day to assign and prove.*
|
||||||
|
|
||||||
**Order.** Assign `zsh` to one server; push; `login-shell.execute@<server> command="uptime"`
|
**Order.** Assign `zsh` to one server; push; `login-shell.execute@<server> command="uptime"`
|
||||||
|
|||||||
@@ -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) |
|
| [`32-what-a-module-declares.md`](32-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md), [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md), superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) |
|
||||||
| [`37-the-operators-machine.md`](37-the-operators-machine.md) | **In progress.** Every configurable thing on a node is a module, the home included; one default per module varied by settings or kept regions; roles a machine has once as seats with tool contracts; one tool runtime per node on the host side | [ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md), [0174](../../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md), [0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), [0176](../../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md), [0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md) |
|
| [`37-the-operators-machine.md`](37-the-operators-machine.md) | **In progress.** Every configurable thing on a node is a module, the home included; one default per module varied by settings or kept regions; roles a machine has once as seats with tool contracts; one tool runtime per node on the host side | [ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md), [0174](../../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md), [0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), [0176](../../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md), [0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md) |
|
||||||
| [`38-building-the-operators-machine.md`](38-building-the-operators-machine.md) | **In progress.** The work of design 37 as packages: the runtime serves many modules, the controller composes one per node, the console becomes its serving mode, the packet filter moves first, then the shell and the service manager — tested on the live mesh by the operator's decision | [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), [0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md), [0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md) |
|
| [`38-building-the-operators-machine.md`](38-building-the-operators-machine.md) | **In progress.** The work of design 37 as packages: the runtime serves many modules, the controller composes one per node, the console becomes its serving mode, the packet filter moves first, then the shell and the service manager — tested on the live mesh by the operator's decision | [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), [0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md), [0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md) |
|
||||||
| [`41-the-shell-and-the-accounts-environment.md`](41-the-shell-and-the-accounts-environment.md) | **In progress.** The shell and the account's environment as modules: an environment module every module contributes variables and `PATH` entries to, shell code contributed to the login shell in named slots, the prompt and plugins as modules, the host giving a login back, and the service manager's module finished | [ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md), [ADR 0204](../../02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md), [ADR 0205](../../02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md) |
|
|
||||||
|
|
||||||
## Not yet written
|
## Not yet written
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
status: resolved
|
status: resolved
|
||||||
opened: 2026-09-26
|
opened: 2026-09-26
|
||||||
located-in: [mesh-controller internal/catalogue, mesh-sdk src/provisioner, mesh-catalog modules/minio]
|
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
|
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
|
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.
|
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
|
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
|
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).
|
|
||||||
Reference in New Issue
Block a user