diff --git a/01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md b/01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md new file mode 100644 index 0000000..a0eee3c --- /dev/null +++ b/01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md @@ -0,0 +1,94 @@ +--- +status: graduated +initiated: 2026-10-04 +touches: + - 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md + - 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md + - 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md + - 02-DECISIONS/0126-a-module-declares-its-own-seats.md + - 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md + - 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md + - 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md + - 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md + - 03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md + - 03-DESIGN/01-to-be/37-the-operators-machine.md + - 03-DESIGN/01-to-be/38-building-the-operators-machine.md + - 04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md +became: + - 02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md + - 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md + - 02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md + - 03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md +--- + +# 025 — How a module plugs into the operator's shell + +## What is investigated + +The shell module writes the mesh's part of the account's shell startup file. But the shell is not the +only module that needs a line there. A prompt theme loads itself from it. A language version manager +sets a variable and sources its loader. A toolchain puts its directory on `PATH`. A desktop module +names the browser. Today all of these sit in one hand-written file, and the shell module as written +carries some of them in its own block and loads others only "if a module placed them". Nothing says +how they get placed. + +This effort asks four things: + +1. **How a module contributes to the shell**: what it declares, who composes it, and in what order it + lands. +2. **Where the environment lives.** Variables and `PATH` entries are facts about the account, not + lines of one shell's syntax. They should reach every shell (interactive or not), the login shell's + `execute` verb, and programs a graphical session starts. +3. **Where the operator's own lines go,** so that assigning the shell module loses nothing the + machine does today. +4. **Which part of a file the mesh owns.** ADR 0174 calls the kept region the operator's; the host and + to-be 38 implement the inverse (the mesh owns a marked block, and everything outside it is the + operator's). The record this becomes says which. + +## Why + +Rolling out the shell module (to-be 38 WP5) was stopped on 2026-10-04 after a review of what assigning +it would do. Measured in [01](01-what-the-shell-file-holds-today.md): + +- Every machine carries the same predecessor-written startup file, so the module's block would be + appended after its own older copy and everything would run twice. +- The block drops lines the machines rely on today. +- Nothing installs the prompt theme or the plugins the block loads. +- The `execute` verb runs a non-interactive login shell, which never reads the file the block is + written into. + +The operator's direction: other modules must be able to plug themselves into the shell; the prompt +becomes its own module; assigning the shell module must lose no functionality; and the environment, +`PATH` above all, needs an answer of its own. + +## What it touches + +- The manifest. A contribution to the shell is either a new use of the existing `contributes` / + `receives` pair or a new gathered field like `jails` (to-be 31). +- The controller's composition, if the controller assembles the text. +- The `login-shell` seat (ADR 0176): what a holder must do with what is contributed to it, and + whether a module or the mesh declares the seat. Possibly a new seat for the environment, beside it + and beside the service manager's (ADR 0177). Research 023 asks the related question of a seat + naming the files its holder owns. +- ADR 0174's wording of the kept region, and ADR 0182's classification of the paths under a home. +- The zsh module, and the modules this makes possible: an environment module, the prompt, a version + manager, a toolchain. + +## Where it stands + +The operator proposed a separate **environment module**: one module, holding a mesh seat of its own, +that alone writes the account's environment. It writes a file that shells source and the service +manager's user environment, from the variables and `PATH` entries every other module contributes to +it. That is the starting position for the environment ([02](02-how-a-module-plugs-in.md) §1, option +E6). It leaves the shell's contribution as shell code only (§2), addressed to the `login-shell` seat, +which moves into the mesh's own seat set beside the new `node-environment` (§6). + +Graduated on 2026-10-04 with one change from the starting positions: the controller, not the +environment module's own code, renders the environment into the module's files, so that the result +is in the declaration before a machine applies it ([ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md), +option 6b). + +## Documents + +- [01 — What the shell file holds today](01-what-the-shell-file-holds-today.md): evidence. +- [02 — How a module plugs in](02-how-a-module-plugs-in.md): the options and the starting position. diff --git a/01-RESEARCH/025-how-a-module-plugs-into-the-shell/01-what-the-shell-file-holds-today.md b/01-RESEARCH/025-how-a-module-plugs-into-the-shell/01-what-the-shell-file-holds-today.md new file mode 100644 index 0000000..49f468b --- /dev/null +++ b/01-RESEARCH/025-how-a-module-plugs-into-the-shell/01-what-the-shell-file-holds-today.md @@ -0,0 +1,103 @@ +# 01 — What the shell file holds today + +Measured 2026-10-04 on the four machines of one installation: two servers and two workstations. All +four have the account's login shell set to zsh, zsh installed from the distribution, and a +predecessor-written startup file. The predecessor is retired, so nothing manages these files any more. + +## The startup file is the same everywhere + +The account's `~/.zshrc` is **byte-identical on all four machines**: 102 lines, one checksum. +`~/.zshrc.local`, which the last line of `~/.zshrc` sources, comes in **two variants**: one shared by +both servers, and one shared by both workstations. So the "per-machine" part is really a +per-*kind*-of-machine part. + +The predecessor produced these from one module with two *flavors*: a prompt flavor and an +autocomplete flavor, each of which swapped in a different local file. Its install hook also: + +- cloned the prompt theme and three plugins from their upstream repositories into `~/.zsh/`; +- installed fonts; +- changed the login shell. + +On the workstations the theme and plugins are still on disk, left over and now owned by nothing. The +servers have none of them. + +## What the 102 lines are + +Sorted by who should own each line once the machine is modules: + +| Lines today | What they are | Natural owner | +|---|---|---| +| `EDITOR`, `VISUAL`, `XDG_CONFIG_HOME`, `PATH` gaining `~/.local/bin` and two script directories | the account's environment | the shell's default, or the environment itself | +| `PATH` gaining a toolchain's directory | environment, for one tool | the toolchain's module | +| a version manager's directory variable plus sourcing its loader | environment *and* shell code | the version manager's module | +| two variables naming the operator's own script library | environment, the operator's own | the operator | +| a variable that turns off an agent's terminal-title handling | environment, for one tool | the agent's module | +| the terminal title hook, keybindings, `dircolors`, the `ls`/`grep` aliases, `ll`/`la`/`l`, a container-run alias, two disk-usage functions, two port aliases | interactive shell behaviour | the shell's default | +| the prompt's instant-prompt cache, the theme, the prompt's own configuration file | shell code, order-sensitive (instant prompt first) | the prompt module | +| autosuggestions, syntax highlighting (and, unloaded, an autocomplete plugin on disk) | shell code, order-sensitive (syntax highlighting last) | a plugin module, or the prompt module | +| sourcing `~/.zshrc.local` | the operator's hook | the operator | + +The workstation variant of the local file adds: + +- more environment: a desktop toolkit theme, a file manager's plugin list, `BROWSER`, `VISUAL` + overridden to a graphical editor, a language toolchain's binary directory on `PATH`; +- two pieces of shell code: one that pads the prompt to the bottom of the terminal under a display, and + one that sources a function file another module places; +- a hook sourcing a further per-node file. + +The server variant holds only that last module-placed source line. + +**Count:** a workstation runs 65 non-comment lines from the two files (53 shared, 12 local); a server +runs 54. Of a workstation's 65: + +- about a quarter (15) are environment; +- about half are interactive defaults no other module cares about; +- the remaining quarter is other modules' code and hooks (a prompt, plugins, a version manager, an + agent's functions), loaded from the shell file only because there was nowhere else to put it. + +## The shell module as written + +The `zsh` module of to-be 38 WP5 (catalogue change, unmerged): + +- writes one block, appended at the end of `~/.zshrc`, holding a subset of the shared file: + - its environment lines, minus the toolchain directory, the version manager and the agent variable; + - the title hook, keybindings and the most common aliases, minus the port aliases; + - guarded `source` lines for the theme and two plugins *if present*; + - the source of `~/.zshrc.local`. +- assigned to any of the four machines, appends that block after the identical lines already there, so + every line in it runs twice, `~/.zshrc.local` included. +- on the servers, the guarded prompt lines find nothing; nothing installs the theme anywhere. + +## Which startup file reaches what + +zsh's startup order, and what each path through it reads: + +| started as | reads | +|---|---| +| interactive login (a console, ssh with a terminal) | `.zshenv`, `.zprofile`, `.zshrc`, `.zlogin` | +| interactive non-login (a new terminal window) | `.zshenv`, `.zshrc` | +| non-interactive login: `zsh -lc …`, what the `execute` verb runs | `.zshenv`, `.zprofile`, `.zlogin`, **not** `.zshrc` | +| non-interactive: a script, `ssh host command` | `.zshenv` only | + +So an environment written into `.zshrc` reaches neither `execute` nor a script. The distribution's +system-wide login profile, which zsh's system `zprofile` sources, only ever *appends* to `PATH` when an +entry is missing. An entry the account's `.zshenv` puts first therefore survives a login. + +A graphical session's programs (a launcher, a bar, a window manager's key bindings) are started from the +display manager and the service manager, not from a shell, and read none of these files. The service +manager's own place for the account's environment is `~/.config/environment.d/`. Today it holds nothing +on any of the four machines, so a program launched from the window manager does not see `PATH` entries +that a terminal does. + +## What the mesh already has for "many modules, one file" + +Measured over the catalogue's 69 module definitions: + +| mechanism | used by | shape | +|---|---|---| +| `contributes` / `receives` | 28 contribute, 15 receive | A consumer contributes **facts** keyed by a requirement. The provider receives all of them as one file in the mesh's own format, and **renders them itself**. "The controller does not know what a reverse proxy is." | +| `listens` / `filtering` | 40 declare listens, 1 composes | The controller derives the whole firewall rule set from every module's ports and writes it where the holder asks. | +| `jails` / `jailing` | 3 declare, 1 composes | Each module supplies its jail **in the tool's own format**. The controller assembles them, sorted, into the one file the holder names. | +| `into: block` on a file | 2 | One module's marked region inside a file something else owns. Text outside the region is kept byte for byte. Placement is at the end, or at the start. | + +None of these is a contribution of shell code or of environment today. diff --git a/01-RESEARCH/025-how-a-module-plugs-into-the-shell/02-how-a-module-plugs-in.md b/01-RESEARCH/025-how-a-module-plugs-into-the-shell/02-how-a-module-plugs-in.md new file mode 100644 index 0000000..ad0f794 --- /dev/null +++ b/01-RESEARCH/025-how-a-module-plugs-into-the-shell/02-how-a-module-plugs-in.md @@ -0,0 +1,197 @@ +# 02 — How a module plugs in + +Six questions, taken one at a time: the environment, shell code, the operator's own lines, order, +who renders, and what a contribution is addressed to. Each has the options weighed and a starting +position. The positions were set with the operator on 2026-10-04 and are what this effort tests, not +what it has decided. + +## 1. The environment: variables and `PATH` + +A variable or a `PATH` entry is a fact about the account. It holds whichever shell is the login shell, +and it is wanted by: + +- every shell, interactive or not; +- the login shell's `execute`; +- a graphical session's programs. + +[01](01-what-the-shell-file-holds-today.md) measures that `.zshrc` reaches only the first kind, and +only interactively. + +| | option | for | against | +|---|---|---|---| +| E1 | Each module writes lines into the shell's rc file (today) | nothing new | misses `execute`, scripts and the graphical session; written in one shell's syntax, so a second shell module starts over | +| E2 | A module contributes environment facts (a variable and its value; a `PATH` entry and its position) **to the login shell**. The holder renders them into its shell's always-read file (`~/.zshenv` for zsh) | reaches every zsh, `execute` included; a contributor names no path and no shell | the graphical session sees none of it; the environment is tied to which module holds the shell; every shell module reimplements the same rendering | +| E3 | E2, and the service-manager holder (ADR 0177) renders the same facts a second time into `~/.config/environment.d/` | the graphical session sees the same `PATH` as the terminal | one fact set, two owners, two renderings that can disagree; the service manager's module gains a duty unrelated to managing services | +| E4 | One composed file in `environment.d` syntax, sourced by the shell with export-all | one file, two readers | ties the shell to the service manager's syntax, which is close to POSIX assignments but not equal (its `${VAR:-default}` and quoting rules differ); a value with a space breaks one reader or the other | +| E5 | Shells take the environment from the service manager's environment generator, which prints the merged `environment.d` | no file of the shell's at all | every shell depends on the service manager and starts a process on every start; the generator's output is unquoted, so a value with a space breaks it | +| **E6** | **An environment module.** A module of its own (working name `node-env`) holds a mesh seat, `node-environment`, and is the only writer of the account's environment. Every module contributes its variables and `PATH` entries to that seat. The holder writes them in each reader's format: a POSIX file of `export` lines that shells source, and the service manager's `~/.config/environment.d/` | the environment no longer depends on which shell holds `login-shell`; one owner and one rendering per format, both from the same facts; the graphical session included without the service manager's module; a contributor addresses "the environment", never a shell; the `PATH` rules (order, de-duplication) live in one module's code, where a test can hold them | one more module and seat, assigned on every node beside the shell; the `login-shell` protocol gains a duty, to source the environment file, which must be written down and checked | + +**Starting position: E6.** It was the operator's proposal on 2026-10-04, and it replaces this +document's first position (E2, then E3). + +- The facts are the contribution. Each format is rendered once, by the one module whose subject is the + environment. +- A shell module's part shrinks to one line in its always-read file: `.zshenv` for zsh, sourcing the + environment module's POSIX file. A bash or fish module writes the same line in its own file, and no + contributor changes when the login shell does. + +Sketched, for a node with zsh, the environment module, and a toolchain: + +``` +toolchain ──contributes PATH entry──▶ node-environment ◀──contributes EDITOR, ~/.local/bin── zsh + │ (held by node-env) + ┌──────────────────┴──────────────────┐ + ▼ ▼ + POSIX export file ~/.config/environment.d/ + ▲ ▲ + sourced from ~/.zshenv read by the service manager + (every zsh, execute too) (the graphical session) +``` + +The shell module still contributes its own environment (`EDITOR`, `XDG_CONFIG_HOME`, `~/.local/bin` on +`PATH`) as a contributor like any other; it does not write those lines itself. Once issue 168 closes, +the values a person varies become settings of whichever module contributes them (ADR 0174). + +## 2. Shell code: a prompt, plugins, a version manager's loader + +This *is* one shell's syntax, and order matters: a prompt's instant-prompt cache must run first, and +syntax highlighting last. + +| | option | for | against | +|---|---|---|---| +| S1 | **A contribution of code for one shell** (the shell it is for, the code, a slot), gathered by the controller and placed inside the holder's block in slot order. The same shape as `jails`, which a module supplies in fail2ban's own format and the controller assembles | a contributor names no path; the order is declared and checkable; unassigning the contributor removes its code at the next composition; a node holding fish simply has no zsh code rendered, and the resolver can say so | the controller gains one more gathered field; code for a shell travels in the declaration (in the clear, so no secrets in it, as for any file) | +| S2 | **A drop-in directory**: each module places its own `~/.zsh/rc.d/NN-name.zsh`, and the shell's block sources the directory | no controller change; each file is its module's own, removed when undeclared | every contributor hard-codes a path inside the shell module's territory, against ADR 0112's spirit; order is a naming convention nothing checks; nothing ties the file to the shell actually being zsh | +| S3 | Contributions as facts the holder renders (`contributes`/`receives` proper) | one mechanism with question 1 | code is not a fact; the holder would only paste it, which is S1 with an extra file | + +**Starting position: S1.** A contribution to the shell carries **only code**, for named shells, each +piece in a slot. Variables and `PATH` entries never go here; they go to the environment (§1). So a +module touching both makes two contributions: + +- A prompt module contributes zsh code in the first slot, and its own configuration file is its own + owned file (ADR 0182). +- A version manager contributes its directory variable to the environment, and its loader as code + for each shell it supports. +- A toolchain contributes a `PATH` entry to the environment and nothing to the shell. + +What has to be settled: what each contribution is *addressed to*. Section 6 covers that. + +## 3. The operator's own lines: the "local override" + +Assigning the shell module must lose nothing the machine does today. That has two halves. + +**What is common is the module's default, not an override.** The startup file is identical on all four +machines ([01](01-what-the-shell-file-holds-today.md)). A line every machine has is the shell module's +default, or another module's contribution. It is not a local override that a person would keep in step +on every machine by hand. Most of today's file therefore moves into the shell module's block and into +the contributions above. Little of it stays the operator's. + +**What is the operator's is everything outside the mesh's block.** The host already works this way: + +- the mesh's region is the marked block; +- text outside it is kept byte for byte, and checked unchanged; +- the region is given back when the module goes. + +| | option | for | against | +|---|---|---|---| +| O1 | The mesh's block at the **start** of the file; the operator's lines after it | the operator's lines run last and win, which is what an override means; already supported (`at: start`) | a file the operator later rewrites must keep the markers; the host refuses a broken pair rather than guess | +| O2 | A named operator region *inside* a file the mesh writes whole (ADR 0174's wording) | the file is entirely the mesh's except one hole | the opposite of what the host implements; a file a person already owns becomes the mesh's | +| O3 | Only `~/.zshrc.local`, sourced from the block; `~/.zshrc` the mesh's whole | one obvious place | takes over a file the person owns today; ADR 0182 classifies the shell's own file as *written into*, not owned | + +**Starting position: O1.** `~/.zshrc.local` keeps working because the operator's own lines source it, +not because the mesh's block does. + +The record this effort becomes corrects ADR 0174's description of the kept region as a **progressive +insight**: the decision stands (a node varies a module by settings or by the operator's own lines, +never by an edit), and only its description of which side is marked changes. + +**The one-off migration** is a person's act, listed in the module's documentation (ADR 0182): + +- remove from today's file every line the block or a contribution now carries; +- keep the rest below the block. + +Until a prompt module and the other contributors exist, the lines they will carry stay among the +operator's own. Nothing is lost at any step. + +## 4. Order + +Order matters only for code. The environment is set before any code runs, because zsh reads +`.zshenv` first. `PATH` entries carry their own position (before or after the system's), which the +environment module orders, not the shell. + +| | option | for | against | +|---|---|---|---| +| R1 | Numbers (`10`, `50`, `90`) | familiar | every contributor guesses a number; collisions are silent | +| R2 | **A few named slots**, `first` / `normal` / `last`, with the module name breaking ties | the prompt says `first` and highlighting says `last` because that is what they mean; the composed result is the same bytes every time | three slots may not be enough | + +**Starting position: R2.** Inside the shell module's block, the order is: + +1. the line sourcing the environment module's file (in `.zshenv`, so it runs for every zsh; the rest + of this list is `.zshrc`); +2. the `first` slot; +3. the shell module's own defaults; +4. the `normal` slot; +5. the `last` slot. + +The operator's lines come after the block, as option O1 says. + +## 5. Who renders: the controller or the holder's code + +There are two different renderings, and E6 lets them be answered differently. + +**The environment** is facts rendered into two fixed formats by the one module whose subject they are. + +- The environment module receives the gathered contributions (the `contributes` / `receives` shape: + facts in the mesh's own format, rendered by the receiver). +- Its own code writes the POSIX file and the `environment.d` file whenever what it receives changes. + That is ADR 0182's third class, written by the module's own process, owned by the account, + atomically. +- The controller learns no shell and no service manager. The `PATH` rules (prepend or append, + de-duplicate, keep the system's entries) are ordinary code with ordinary tests. +- To settle: what runs that code when the received file changes. The candidates are a host action + that restarts on the received file, or a subscription through the runtime (ADR 0198). + +**Shell code** is not facts. It is text in the shell's own syntax, assembled in slot order, which is +what the controller already does for fail2ban jails: sort the pieces and concatenate them into the +holder's region. The controller assembles; it never interprets the code. This keeps the shell module +bundle-free for its files, and keeps the composed result visible in the declaration before a machine +applies it. + +## 6. What a contribution is addressed to + +Under E6 there are two addressees: the environment and the login shell. + +| | option | for | against | +|---|---|---|---| +| A1 | **Seats**: environment facts to `node-environment`, shell code to `login-shell`. Each seat's protocol says what its holder does with what is contributed to it | a contributor depends on a role ("the environment", "the login shell"), never on zsh or on one module; works the same for any holder | `login-shell` today is declared by the zsh module itself (ADR 0126). A second shell module may only claim it, never declare it, and the seat exists only while zsh's definition is registered | +| A2 | Requirements the modules provide (`contributes` keyed by them, as the reverse proxy is) | an existing mechanism | a contributor on a node without the provider fails to resolve, though a toolchain's `PATH` entry with no environment module is merely unwritten | + +**Starting position: A1, both seats in the mesh's own seat set** beside the service manager. + +- `node-environment` is new, and is the mesh's from the start. +- `login-shell` moves there from the zsh module's definition. A shell is as universal a role as a + service manager, and a protocol that now carries duties (render the shell code contributed to it, + source the environment file) should not depend on one module's registration. + +Research 023 (a seat's protocol naming what its holder owns) is the general form of this: the +environment seat would own the two environment files, and the login-shell seat the shell's +startup-file region. The two efforts should not decide it twice. + +## Open questions + +- Whether a contribution may be conditional on a capability: the workstation-only environment (a + browser, a toolkit theme) is a desktop module's contribution, which arrives only where that module is + assigned. Measured, this may need nothing new. +- What a node without the environment module does with environment contributions: refuse them at + resolve, or leave them unwritten and say so. The position here is to say so; a missing `PATH` entry + is a visible gap, not a broken machine. +- Whether the operator's own variables (the script-library paths in [01](01-what-the-shell-file-holds-today.md)) + are the operator's lines below the shell block, or a kept region of the environment module's file. + The first needs nothing new, but reaches only interactive zsh. +- How the prompt module and a plugin module divide the plugins. Packaging decides it as much as + ownership: the plugins come from upstream repositories, not distribution packages, on these machines. +- Whether the `execute` verb should read the interactive file at all once the environment is in + `.zshenv`. The position here is no: a non-interactive login shell plus the environment is what a + command needs, and the prompt's code should not run for it. +- How a contribution reaches a second shell assigned beside the holder, which to-be 38 WP5 names as the + first follow-up record. Under A1 a non-holder renders nothing, so the question becomes whether a + non-holding shell module may render contributions for interactive use. diff --git a/02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md b/02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md index 89628c8..36162f9 100644 --- a/02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md +++ b/02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md @@ -9,6 +9,8 @@ extends: 02-DECISIONS/0011-managed-files-are-generated-never-edited.md # 174. A node varies a module through settings and kept regions, never through an edit +> **The mechanism changed — 2026-10-04, by [ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md).** Where this record calls a kept region *a marked block in which the operator's own lines are kept*, read the inverse, which is what the host built: the mesh's region is the marked block, and every line outside it is the operator's, kept byte for byte and given back when the module goes. The decision stands: a node varies a module by settings and by the operator's own lines, never by an edit. + ## Context [ADR 0011](0011-managed-files-are-generated-never-edited.md) says a managed file is derived and an diff --git a/02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md b/02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md index da3d9e6..89d9a24 100644 --- a/02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md +++ b/02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md @@ -9,6 +9,8 @@ extends: 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md # 176. The login shell is a node seat held by one shell module, and `execute` is its contract +> **The mechanism changed — 2026-10-04, by [ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md).** The seat is no longer declared by the shell modules (§1). It is `node-login-shell`, in the mesh's own seat set, which a shell module claims. Its holder also places the shell code other modules contribute, and sources the account's environment ([ADR 0203](0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md)). What stands: one holder per node, the login shell set by the `user` shape and given back, `execute` as the contract, and any node may call it. + ## Context [ADR 0040](0040-what-a-module-is.md) names the shell as its example of a *shared* seat: bash, zsh diff --git a/02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md b/02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md new file mode 100644 index 0000000..78066cf --- /dev/null +++ b/02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md @@ -0,0 +1,131 @@ +--- +topic: what runs on it +status: accepted +date: 2026-10-04 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md +--- + +# 203. The account's environment is one module's, and every module contributes to it + +## Context + +A variable or a `PATH` entry is a fact about the operator's account. A toolchain needs its directory +on `PATH`, a version manager needs a variable naming its directory, an agent needs a variable that +turns one of its behaviours off, and the shell sets an editor. Today every one of these is a line of +one shell's syntax in one hand-written startup file. [Research 025](../01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md) +measured on four machines: + +- about a quarter of the 65 lines a workstation runs at shell start are environment; +- written into `.zshrc`, that environment reaches only interactive zsh. It misses the login shell's + `execute` verb ([ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)), + every script, and every program a graphical session starts; +- the service manager's place for the account's environment, `~/.config/environment.d/`, holds + nothing on any machine. + +The shell module as first written carried some of these lines in its own block and dropped the rest. + +## Considered Options + +1. **Each module writes its own lines into the shell's startup file.** Rejected: one shell's syntax, + read by one kind of start, and the same lines rewritten by every shell module. +2. **Contribute the facts to the login shell, whose holder renders them.** Rejected: the environment + then depends on which module holds the shell, every shell module renders the same facts again, and + the graphical session sees nothing. +3. **Option 2, and the service manager's holder renders the same facts a second time** into + `environment.d`. Rejected: one fact set with two owners, whose renderings can disagree, and a + duty for the service manager unrelated to managing services. +4. **One file in `environment.d` syntax, sourced by shells.** Rejected: that syntax is close to + POSIX assignment but not equal, and a value one reader accepts breaks the other. +5. **Shells read the service manager's environment generator.** Rejected: every shell start then + runs a process and depends on the service manager, and the output is unquoted. +6. **A module of its own holds the environment.** One mesh seat, held by one module per node, + whose files are the account's environment. Every module contributes facts to it, and those facts + are written in each reader's format. Chosen. It was the operator's proposal. + +Within option 6, two ways to write the files: + +- **a. The holder's own code renders what it receives.** This was research 025's starting position. + Rejected: the code needs something to run it whenever a contribution changes, and the result exists + only after a machine has applied and run it. +- **b. The controller renders the facts into the holder's files,** in two named formats, at + composition. Chosen. The result is in the declaration before any machine applies it, nothing has to + trigger anything, and the two formats are standards: POSIX shell assignment and the service + manager's `environment.d`. The controller learns no shell. It writes an assignment in a standard + syntax, as it already writes a fail2ban stanza a module supplied. + +## Decision + +**1. The environment is a node seat, `node-environment`, in the mesh's own set.** One module per node +holds it, and it is the only writer of the account's environment. The first holder is a module of its +own (working name `node-env`), with no package and no process. + +**2. Any module contributes to it with `environment`:** + +- **`variables`:** names and values. A name is a POSIX variable name and never `PATH`. A value is + literal; it may use `${machine:…}`, resolved first, and may not contain `$`, a quote, a backslash or + a line break. The only expansion is the mesh's own, so the two formats cannot read one value + differently. +- **`path`:** entries, each placed at the `start` or the `end` of the account's `PATH`. + +**3. The holder places the rendered environment with two placeholders** in its own files: + +- **`${environment:posix}`** renders lines a POSIX shell sources: + - every variable exported; + - every `PATH` entry added only if missing, so sourcing twice changes nothing. +- **`${environment:systemd}`** renders the same facts as the service manager's user environment, with + the account's existing `PATH` kept between the start and the end entries. + +Each rendered line names the module that contributed it, so the file answers *where did this come +from*. Contributions are ordered by module name, and then in the order a module declared them. + +**4. The seat's protocol fixes where the POSIX file is:** `~/.config/mesh/environment.sh` under the +account's home. A shell sources that path without knowing which module wrote it. The service +manager's file is `~/.config/environment.d/50-mesh.conf`. + +**5. Refused at composition:** + +- two modules on one node setting the same variable, both named; +- an environment placeholder in a module that does not claim `node-environment`. + +A node with contributions and no holder writes them nowhere. The holder's absence is visible in the +node's assignments, and no contributor is refused for it, because a missing `PATH` entry is a gap, not +a broken machine. + +## Consequences + +- A shell's part in the environment is one line in its always-read startup file, sourcing the + POSIX file. A second shell module writes the same line in its own syntax, and no contributor + changes when the login shell does. +- The graphical session sees the same `PATH` as the terminal, from the same facts. +- The controller gains one gathered field and two renderers. Both are tested byte for byte, like + the jails a node composes ([to-be 31](../03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md)). +- What a person sets for themselves stays theirs: variables of their own sit in their own lines of + their shell's file, read after the mesh's. +- **What got harder:** a value that needs another variable expanded (`$HOME`, `$XDG_CONFIG_HOME`) + must be written with the mesh's own `${machine:…}` facts, or it is refused. Expansion at shell start + is exactly what made one value mean two things in two readers. +- Once [issue 168](../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md) + closes, a value a person varies becomes a setting of the module that contributes it + ([ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)). + +## How it is checked + +| Rule | Checked by | +|---|---| +| Both renderings, byte for byte, from a fixed set of contributions | the controller's environment tests | +| Sourcing the POSIX rendering twice leaves `PATH` unchanged | the same tests, running `sh` over the rendering | +| A variable set by two modules is refused, naming both | the controller's resolve test | +| An environment placeholder outside the holder is refused | the catalogue check, which registration runs | +| A value with `$`, a quote, a backslash or a line break is refused | the manifest's parse test | +| The zsh holder sources the path the seat fixes | the catalogue's zsh test | + +## References + +- [Research 025](../01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md) +- [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md), + [ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md), + [ADR 0177](0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md), + [ADR 0182](0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md) +- [To-be 41](../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md) diff --git a/02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md b/02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md new file mode 100644 index 0000000..1a67292 --- /dev/null +++ b/02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md @@ -0,0 +1,131 @@ +--- +topic: what runs on it +status: accepted +date: 2026-10-04 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md +--- + +# 204. A module contributes shell code to the login shell in named slots, and the login shell is the mesh's seat + +## Context + +Some of what a shell runs at start is code in that shell's own syntax, and it belongs to other +modules: + +- a prompt theme loads itself and its configuration; +- plugins load themselves; +- a version manager sources its loader. + +Order matters: a prompt's instant-prompt cache must run first, and syntax highlighting last. The +predecessor kept all of this in one file per machine, and installed the theme and plugins by cloning +them in a hook. +[Research 025](../01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md) measured that file +as byte-identical on four machines. It carries: + +- the shell's defaults; +- code belonging to four other pieces of software; +- a handful of the operator's own lines. + +Nothing gave the other pieces a way in. + +Two further facts bear on the seat itself: + +- **The seat is declared by the zsh module** ([ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md) + §1, under [ADR 0126](0126-a-module-declares-its-own-seats.md)). The controller refuses a second + module declaring a seat name, so fish or bash could only ever claim it, and the seat exists only + while zsh's definition is registered. +- **The kept region is the other way round.** [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md) + describes a kept region as a marked block holding the operator's lines. The host built the inverse: + the mesh's region is the marked block, and every byte outside it is kept, verified unchanged, and + given back when the module goes. + +## Considered Options + +1. **A drop-in directory** that each module places a file in, and the shell sources. Rejected: every + contributor names a path inside the shell module's territory + ([ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md)); order becomes a naming + convention nothing checks; and nothing ties the file to the shell actually being the one it is + written for. +2. **Facts the holder renders**, through `contributes` / `receives`. Rejected: code is not a fact, + and the holder would only paste it. +3. **Code contributed for a named shell in a named slot, assembled by the controller into the + holder's file.** This is what the controller already does for fail2ban jails: each module supplies + text in the tool's own format, and the controller sorts and concatenates it into the holder's file + without interpreting it. Chosen. + +For order, numbers (`10`, `50`, `90`) were rejected: every contributor guesses one, and collisions are +silent. **Three named slots** were chosen: `first`, `normal`, `last`. + +## Decision + +**1. `node-login-shell` is a node seat in the mesh's own set,** with the verb `execute`. It replaces +the module-declared `login-shell`. Everything else ADR 0176 decided stands: the holder sets the +account's login shell through the `user` shape, `execute` is the contract, and any node may call it. +A shell module claims the seat; none declares it. + +**2. Any module contributes shell code with `shell`:** entries naming the shell they are for (`zsh`, +`bash`, `fish`), the slot, and the code. The controller does not read the code. + +**3. The holder places the code with placeholders** in its own files: `${shell::}`. 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) diff --git a/02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md b/02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md new file mode 100644 index 0000000..4c60c5a --- /dev/null +++ b/02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md @@ -0,0 +1,69 @@ +--- +topic: what runs on it +status: accepted +date: 2026-10-04 +deciders: jochen +reconstructed: false +--- + +# 205. Software the distribution does not package ships as a pinned archive of the module's own + +## Context + +The prompt theme the operator uses is not in the distribution's repositories. Its two plugins and an +autocomplete plugin are. The predecessor installed all four by running `git clone` against their +upstream repositories from an install hook. That way: + +- the version on a machine was whatever upstream's default branch held the day the hook ran; +- two machines set up a week apart could differ; +- a machine with no route to upstream failed its install. + +The mesh already has a pinned, delivered form for a module's own files: an **archive artifact** built +from a directory of the module's source, delivered by the artifact store, unpacked by the host's +`archive` resource, and pinned by digest. One showcase module uses it. + +## Considered Options + +1. **Clone from upstream on the machine,** as the predecessor did. Rejected: unpinned, unreproducible, + and it needs upstream reachable from every machine. +2. **Build from the distribution's user repository.** Rejected: the host installs packages from the + distribution's own repositories. A user-repository build is a toolchain on every machine for one + theme. +3. **Vendor a pinned upstream release into the module's directory and ship it as the module's archive + artifact.** Chosen. The release and its version are named in the module, its licence travels with + it, and every machine gets the same bytes from the mesh's own store. + +## Decision + +**A module whose software the distribution does not package carries a pinned upstream release in its +own source directory and ships it as an archive artifact.** + +- The module's documentation names the upstream, the version and the licence. +- The host unpacks it with the `archive` resource into a directory the module owns. +- An upgrade is a change to the module, reviewed like any other. + +Software the distribution *does* package is installed as a package; a vendored copy of it is +refused in review. + +## Consequences + +- The catalogue grows by the size of what it vendors: 1.4 MB for the prompt theme at the pinned + release. +- Upstream's security fixes reach a machine only when somebody updates the module. That is the same + trade every pinned dependency makes, and it is visible: the version is in the module. +- **What got harder:** a vendored program that downloads more at run time, as the prompt theme does + for its git status helper, still fetches that part from upstream on first use. This record pins + what the mesh ships, not what the software fetches for itself. The module's documentation says so. + +## How it is checked + +| Rule | Checked by | +|---|---| +| The archive is pinned by digest | the host's declaration validation, which refuses an archive without one | +| The upstream, version and licence are named | review of the module's documentation; the module's test asserts the licence file is in the archive | +| Packaged software is not vendored | review | + +## References + +- [ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md) +- [To-be 41](../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 9f2880c..80a0de4 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -301,6 +301,9 @@ python3 00-META/checks/index.py fail if stale - **0197** — [Every tool announces itself on the bus, in the NATS services protocol](0197-every-tool-announces-itself-on-the-bus-in-the-nats-services-protocol.md) - **0198** — [A module's long-running code is launched by the node's runtime, and reaches the bus through it](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md) - **0201** — [A module keeps its current state in key-value buckets it declares, and reaches them through the runtime](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md) +- **0203** — [The account's environment is one module's, and every module contributes to it](0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md) +- **0204** — [A module contributes shell code to the login shell in named slots, and the login shell is the mesh's seat](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md) +- **0205** — [Software the distribution does not package ships as a pinned archive of the module's own](0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md) ### How it is built diff --git a/03-DESIGN/01-to-be/37-the-operators-machine.md b/03-DESIGN/01-to-be/37-the-operators-machine.md index 904a2ae..f5ab9a5 100644 --- a/03-DESIGN/01-to-be/37-the-operators-machine.md +++ b/03-DESIGN/01-to-be/37-the-operators-machine.md @@ -2,7 +2,7 @@ layer: to-be status: in-progress code: [mesh-host, mesh-controller, mesh-tools, mesh-catalog] -updated: 2026-10-02 +updated: 2026-10-04 decisions: - 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md - 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md @@ -33,12 +33,16 @@ This is the design [to-be 29](29-a-node-has-operator-accounts.md) §2 called *a Worked on the first one, a shell. The `zsh` module declares: - a **package**, `zsh`; -- **files under the home**, owned by the account: the shell's rc file with the module's default - configuration, carrying a kept region for the operator's own lines, and `${setting:…}` - placeholders for the few values a node varies; the account and its home are machine facts the - controller resolves ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), - to-be 29 §2); -- a **seat declaration**, `login-shell`, node-scoped, with its one verb; and a **claim** on it; +- **files under the home**, owned by the account: the mesh's block at the start of the shell's rc + file with the module's default configuration and the slots other modules' code lands in, the + operator's own lines kept after it, and `${setting:…}` placeholders for the few values a node + varies; the account and its home are machine facts the controller resolves + ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), + to-be 29 §2). Its environment is a contribution to the environment module, not lines of its own + ([ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md), + [ADR 0204](../../02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md), + [to-be 41](41-the-shell-and-the-accounts-environment.md)); +- a **claim** on the mesh's node-scoped seat `node-login-shell`, with its one verb; - a **`user` shape** naming the shell, applied only where the module holds the seat; - a **tools bundle**, the artifact kind for interpreted code, with `execute` and the module's own `show-config`. @@ -80,7 +84,8 @@ root escalates itself. ## 4. The seats of the environment -Decided now: **`login-shell`** (module-declared; zsh, fish, bash; verb `execute`) and +Decided now: **`node-login-shell`** (the mesh's own, ADR 0204; zsh, fish, bash; verb `execute`), +**`node-environment`** (the mesh's own, ADR 0203; the environment module; no verbs) and **`node-service-manager`** (the mesh's own; systemd; verbs over units in both scopes). The rest 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 diff --git a/03-DESIGN/01-to-be/38-building-the-operators-machine.md b/03-DESIGN/01-to-be/38-building-the-operators-machine.md index 2cd058c..005243e 100644 --- a/03-DESIGN/01-to-be/38-building-the-operators-machine.md +++ b/03-DESIGN/01-to-be/38-building-the-operators-machine.md @@ -15,6 +15,7 @@ decisions: - 02-DECISIONS/0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md - 02-DECISIONS/0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md - 02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md + - 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md --- # 38. Building the operator's machine @@ -330,6 +331,13 @@ tool of each. ## WP5 — The shell, on a server first +*Replaced on 2026-10-04 by [to-be 41](41-the-shell-and-the-accounts-environment.md).* A review before +assigning found that the shell module would duplicate every machine's existing startup file, drop +lines from it, leave the prompt uninstalled, and could not be unassigned +([issue 228](../../04-ISSUES/228-a-login-the-mesh-set-is-never-given-back/00-report.md)). The shell, +its environment, the modules that plug into it, and the host's fix are built and proven there. What +follows is the original plan, kept for the record. + *mesh-catalog #224, already written. Half a day to assign and prove.* **Order.** Assign `zsh` to one server; push; `login-shell.execute@ command="uptime"` diff --git a/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md b/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md new file mode 100644 index 0000000..49c2eb0 --- /dev/null +++ b/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md @@ -0,0 +1,238 @@ +--- +layer: to-be +status: in-progress +code: [mesh-host, mesh-controller, mesh-catalog] +updated: 2026-10-04 +decisions: + - 02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md + - 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md + - 02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md + - 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md + - 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md + - 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md +--- + +# 41. The shell and the account's environment + +What it takes for the operator's shell to be modules, without a machine losing anything it does +today. This design replaces the shell half of +[to-be 38](38-building-the-operators-machine.md) WP5, and finishes the service-manager module of WP6 +short of its user-scoped units. The decisions are [ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md) +(the environment), [ADR 0204](../../02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md) +(shell code and the seat) and [ADR 0205](../../02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md) +(vendored software). The evidence is [research 025](../../01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md). + +## What a node with a shell looks like + +``` + toolchain, agent, … prompt, plugins, version manager + │ environment │ shell (zsh, slot) + ▼ ▼ + node-environment ── holds ── node-env node-login-shell ── holds ── zsh + │ │ + ├─▶ ~/.config/mesh/environment.sh ◀─ sourced from zsh's block in ~/.zshenv + └─▶ ~/.config/environment.d/50-mesh.conf ◀─ read by the account's service manager + │ + ~/.zshrc: the mesh's block FIRST — defaults and the + three slots — then the operator's own lines, kept +``` + +**The environment module** (`node-env`) holds `node-environment`. It has no package and no process. +Its two files are written by the host from placeholders the controller fills. + +**The shell module** (`zsh`) holds `node-login-shell`. It: + +- installs the package; +- sets the login shell through the `user` shape; +- writes two blocks: + - one in `.zshenv`, sourcing the environment; + - one at the start of `.zshrc`, holding the defaults every machine shares today: the title, the + keybindings, the aliases and the two small functions, with the three slots in place; +- contributes its own environment: the editor, the configuration home, and `~/.local/bin` plus the + two script directories on `PATH`; +- serves `execute` and its own `zsh_config`. + +**The prompt module** (`powerlevel10k`) ships the theme as a pinned vendored archive, and the prompt's +configuration as its own file in a directory it owns. It contributes the zsh code that loads both. + +**Two plugin modules** (`zsh-autosuggestions`, `zsh-syntax-highlighting`) each install their +distribution package and contribute one line. Syntax highlighting goes in the `last` slot, which is +what its upstream asks for. + +**What stays the operator's** is everything below the block in `.zshrc`, and `~/.zshrc.local`, which +the operator's lines source as they do today. On every machine today that means: + +- the version manager's lines and the toolchain's `PATH` entry, until those modules exist; +- the two variables naming the operator's own script library; +- the agent's title variable; +- the port aliases; +- the workstation's desktop variables, which live in `~/.zshrc.local` already. + +Nothing is lost at any step, because a line moves out of the operator's part only when a module +carries it. + +**The migration is a person's act**, listed in the zsh module's documentation (ADR 0182): after the +first push, delete from `.zshrc` the lines the block now carries. Until then they run twice, which is +harmless and visible. + +## Work packages + +``` +WP1 the host gives a login back (mesh-host) issue 228 +WP2 the controller composes environment and shell code (mesh-controller) +WP3 the modules (mesh-catalog) needs WP2 to resolve +WP4 the service manager's module, finished (mesh-catalog) independent +WP5 assign and prove (operator-gated) needs WP1–WP3 merged and rolled +``` + +WP1, WP2 and WP4 are independent, and are built in parallel on one feature branch per repository +([playbook 07](../../00-META/process/07-feature-branches.md)). WP3 is written in parallel and proven +against WP2's controller before anything is published. + +## WP1 — The host gives a login back + +*mesh-host. Half a day. [Issue 228](../../04-ISSUES/228-a-login-the-mesh-set-is-never-given-back/00-report.md).* + +**What changes.** + +- The `user` shape records, in its applied record, the login shell it found whenever it changes it. +- Removing a `user` never deletes the account, whether or not the mesh created it. If the account's + shell is still the one the mesh set, and the recorded shell is still executable, the recorded shell + is set back. Otherwise the shell is left as it is, and the outcome says why. +- Before a shell is set, it is refused unless it is executable and listed among the machine's shells. + The exception is a shell that refuses logins (`nologin`, `false`): the distribution does not list + those, and the controller's own account uses one, so it need only be executable. The refusal fails + that resource and leaves the account untouched. +- A directory the host creates on the way to a file, a block or an archive inside an account's home + belongs to that account, the home itself included when the host makes it. A directory that was + already there keeps its owner and mode (ADR 0182). Until this, a fresh account's `~/.config` or + `~/.local/share` would have been created as root's. +- Giving the shell back is reported, never fatal. A failed `usermod` on removal is named in the + outcome and the record is dropped, because a fatal removal is exactly the wedge issue 228 is about. + +**Proof.** The host's tests: + +- an undeclared `user` no longer stops the apply; +- the found shell comes back; +- a shell changed by a person since is left alone; +- a missing shell is refused before `usermod` runs; +- a created account survives its removal. + +## WP2 — The controller composes environment and shell code + +*mesh-controller. One to two days.* + +**What changes.** + +- **The seat table.** It gains `node-environment` (node scope, no verbs) and `node-login-shell` + (node scope, the verb `execute`). A module may no longer declare a seat named `login-shell` or + `node-login-shell`. Both new seats are seeded into a live store by the existing additive seeding. +- **The manifest.** It gains two contribution fields, each refused at parse when malformed: + - `environment`, with `variables` and `path`: a variable name must be a POSIX name and not `PATH`; + a value may not contain `$`, a quote, a backslash or a line break; a path entry's place is + `start` or `end`; + - `shell`: each entry names a known shell, a known slot, and non-empty code. +- **Composition.** It fills `${environment:posix}`, `${environment:systemd}` and + `${shell::}` 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@ 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. diff --git a/03-DESIGN/01-to-be/README.md b/03-DESIGN/01-to-be/README.md index fba266c..125651c 100644 --- a/03-DESIGN/01-to-be/README.md +++ b/03-DESIGN/01-to-be/README.md @@ -43,6 +43,7 @@ document is written and this one's status becomes `implemented`. | [`32-what-a-module-declares.md`](32-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md), [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md), superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) | | [`37-the-operators-machine.md`](37-the-operators-machine.md) | **In progress.** Every configurable thing on a node is a module, the home included; one default per module varied by settings or kept regions; roles a machine has once as seats with tool contracts; one tool runtime per node on the host side | [ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md), [0174](../../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md), [0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), [0176](../../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md), [0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md) | | [`38-building-the-operators-machine.md`](38-building-the-operators-machine.md) | **In progress.** The work of design 37 as packages: the runtime serves many modules, the controller composes one per node, the console becomes its serving mode, the packet filter moves first, then the shell and the service manager — tested on the live mesh by the operator's decision | [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), [0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md), [0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md) | +| [`41-the-shell-and-the-accounts-environment.md`](41-the-shell-and-the-accounts-environment.md) | **In progress.** The shell and the account's environment as modules: an environment module every module contributes variables and `PATH` entries to, shell code contributed to the login shell in named slots, the prompt and plugins as modules, the host giving a login back, and the service manager's module finished | [ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md), [ADR 0204](../../02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md), [ADR 0205](../../02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md) | ## Not yet written diff --git a/04-ISSUES/228-a-login-the-mesh-set-is-never-given-back/00-report.md b/04-ISSUES/228-a-login-the-mesh-set-is-never-given-back/00-report.md new file mode 100644 index 0000000..9be8a57 --- /dev/null +++ b/04-ISSUES/228-a-login-the-mesh-set-is-never-given-back/00-report.md @@ -0,0 +1,52 @@ +--- +status: located +opened: 2026-10-04 +located-in: + - mesh-host +fixed-by: +amended-design: +--- + +# 228 — A login the mesh set is never given back, and undeclaring one stops the node applying + +## What was observed + +2026-10-04. Before the shell module of to-be 38 WP5 was assigned anywhere, a review traced what the +host does with the `user` shape the module declares (the operator account, with the login shell zsh) +on three events: first assign, a later push, and unassign. + +1. **Undeclaring a `user` stops the node applying anything, for good.** + - The host's removal has no case for a `user`, so the orphaned record fails with "no way to remove". + - Orphans are removed before the declaration's first resource, and that failure aborts the apply. + - The record stays in the host's store, so every later apply fails the same way. + + This was reproduced in a throwaway test against the host's code: the apply produced no outcomes, + and an unrelated file in the same declaration was not written. Renaming the resource's id has the + same effect. A showcase module carries a `user` today and is exposed to it too. +2. **The shell the account had is never recorded.** [ADR 0176](../../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md) + §2 says the host "gives back [the login shell] when the holding moves". The host keeps nothing to + give back. +3. **A login shell is set whether or not it exists.** A failed package install does not stop the + resources after it. `usermod --shell` on the distribution only warns about a missing or + non-executable shell, and succeeds. The host's read-back compares the user database's string, + which matches. So an account can be pointed at a shell that is not there, and console, ssh and + display-manager logins then fail. No machine hit this, because zsh was already installed on all + four. + +## Why it matters beyond this instance + +Unassigning any module with a login in it, the case the mesh promises is ordinary, wedges the +machine's applies until a person edits the host's store. It is the same class of failure as an +earlier archive that could not be removed: a shape the host can create and cannot take away. + +## Located + +mesh-host, `internal/apply`: `remove()` has no `user` case, and `applyUser` neither records the shell +it replaced nor checks the shell it sets. The fix is set out in +[to-be 41](../../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md) WP1: + +- a removal that never deletes an account; +- the login shell given back, if it is still the one the mesh set and the recorded one still exists; +- a shell refused before it is set unless it is executable and listed among the machine's shells + (a shell that refuses logins need only be executable, since the distribution does not list it and + the controller's own account uses one).