The operator's proposal: one module, holding a mesh seat node-environment, is the only writer of the account's environment. It renders contributed variables and PATH entries as a POSIX file shells source and as environment.d for the graphical session. The shell's contribution shrinks to shell code; the shell render-then-service-manager-again positions become options weighed against it.
198 lines
15 KiB
Markdown
198 lines
15 KiB
Markdown
# 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.
|