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 index 0b4bc1b..a0eee3c 100644 --- 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 @@ -1,5 +1,5 @@ --- -status: active +status: graduated initiated: 2026-10-04 touches: - 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md @@ -14,7 +14,11 @@ touches: - 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: [] +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 @@ -79,6 +83,11 @@ it. That is the starting position for the environment ([02](02-how-a-module-plug 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. 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..2cf6ab4 --- /dev/null +++ b/02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md @@ -0,0 +1,68 @@ +--- +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. The prompt theme is about two megabytes. +- 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..90be731 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 225](../../04-ISSUES/225-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..4f16bf4 --- /dev/null +++ b/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md @@ -0,0 +1,230 @@ +--- +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 225 +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 225](../../04-ISSUES/225-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 refusal fails that resource and leaves the account untouched. + +**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/225-a-login-the-mesh-set-is-never-given-back/00-report.md b/04-ISSUES/225-a-login-the-mesh-set-is-never-given-back/00-report.md new file mode 100644 index 0000000..66a5ea9 --- /dev/null +++ b/04-ISSUES/225-a-login-the-mesh-set-is-never-given-back/00-report.md @@ -0,0 +1,50 @@ +--- +status: located +opened: 2026-10-04 +located-in: + - mesh-host +fixed-by: +amended-design: +--- + +# 225 — 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.