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 d5f6463..0b4bc1b 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 @@ -4,6 +4,8 @@ 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 @@ -61,10 +63,21 @@ becomes its own module; assigning the shell module must lose no functionality; a `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. Research 023 asks the related question of a seat + 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: the prompt, a version manager, a toolchain. +- 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). ## Documents 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 index 4e67b4e..ad0f794 100644 --- 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 @@ -1,8 +1,9 @@ # 02 — How a module plugs in -Four questions, taken one at a time: the environment, shell code, the operator's own lines, and -ordering. 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. +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` @@ -19,20 +20,36 @@ 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). The controller gathers them per node. The login-shell 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; a second shell renders the same facts | a graphical session still sees none of it | -| E3 | E2, and the gathered facts are **also** written as the service manager's user environment (`~/.config/environment.d/`) | the graphical session sees the same `PATH` as the terminal | two renderings of one fact set; the user manager reads it only when it starts | -| E4 | One composed file in the service manager's `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 | +| 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: E2, with E3 as the second step.** +**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 reader renders them in its own syntax. This is the - `contributes` / `receives` boundary applied once more: the controller does not know what a shell is. -- E3 then needs no new contribution, only a second renderer. Its natural owner is the service-manager - seat's holder (ADR 0177), not the shell. -- Whether the controller does the rendering or the holder's own code does is question 5 below. +- 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. -What the shell module itself sets (`EDITOR`, `XDG_CONFIG_HOME`, `~/.local/bin` on `PATH`) is the -shell module's own contribution, made the same way rather than written as lines. Once issue 168 closes, +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 @@ -46,18 +63,17 @@ syntax highlighting last. | 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.** It makes one contribution, *to the shell*, with three parts: variables, -`PATH` entries, and code for named shells. +**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, and its loader as code for each shell it - supports. -- A toolchain contributes a `PATH` entry and nothing else. +- 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 a contribution is *addressed to*. It could be the `login-shell` seat, so -that whichever module holds it renders the contributions, or a requirement the shell module provides. -Section 6 covers that. +What has to be settled: what each contribution is *addressed to*. Section 6 covers that. ## 3. The operator's own lines: the "local override" @@ -98,8 +114,9 @@ operator's own. Nothing is lost at any step. ## 4. Order -Order matters only for code. Variables are set before any code runs, and `PATH` entries carry their own -position: before or after the system's. +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 | |---|---|---|---| @@ -108,7 +125,8 @@ position: before or after the system's. **Starting position: R2.** Inside the shell module's block, the order is: -1. the environment; +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; @@ -118,42 +136,57 @@ The operator's lines come after the block, as option O1 says. ## 5. Who renders: the controller or the holder's code -The holder could render the contributions itself: +There are two different renderings, and E6 lets them be answered differently. -- by its bundle writing the files when what it receives changes (ADR 0182's third class, "written by - the module's own process"); -- or by the controller rendering them into the holder's block at composition, as it assembles `jails`. +**The environment** is facts rendered into two fixed formats by the one module whose subject they are. -**Starting position: the controller assembles, the holder states the format.** +- 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). -- *Assembling* is what the controller already does for jails: sort the pieces and concatenate them into - the holder's region. -- *Formatting* an environment fact as a line of shell is the holder's knowledge. The holder declares it - as a pattern beside its claim, for example "a variable renders as `export NAME=value`", so the - controller learns no shell. -- This keeps the module bundle-free for its files, and keeps the composed result visible in the - declaration before a machine applies it. - -To be tested: whether a pattern can express `PATH` composition (prepend versus append, de-duplication) -cleanly, or whether that one case forces holder code. +**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 | The `login-shell` seat: the seat's protocol says a holder renders the shell contributions on its node | a contributor depends on "the login shell", not on zsh; works the same for any holder | the seat 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 | A requirement the shell module provides (`contributes` keyed by it, as the reverse proxy is) | an existing mechanism | a contributor on a node with no shell module fails to resolve, though a prompt with no shell is merely useless | +| 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,** with the seat moved into the mesh's own seat set beside the service manager. -A shell is as universal a role as a service manager, and a protocol that now carries a rendering duty -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, and the two should not decide it twice. +**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