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..d5f6463 --- /dev/null +++ b/01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md @@ -0,0 +1,72 @@ +--- +status: active +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/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: [] +--- + +# 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. 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. + +## 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..4e67b4e --- /dev/null +++ b/01-RESEARCH/025-how-a-module-plugs-into-the-shell/02-how-a-module-plugs-in.md @@ -0,0 +1,164 @@ +# 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. + +## 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). 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 | + +**Starting position: E2, with E3 as the second step.** + +- 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. + +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, +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.** It makes one contribution, *to the shell*, with three parts: variables, +`PATH` entries, and code for named shells. + +- 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. + +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. + +## 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. Variables are set before any code runs, and `PATH` entries carry their own +position: before or after the system's. + +| | 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 environment; +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 + +The holder could render the contributions itself: + +- 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`. + +**Starting position: the controller assembles, the holder states the format.** + +- *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. + +## 6. What a contribution is addressed to + +| | 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 | + +**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. + +## 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. +- 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.