Files
hq/01-RESEARCH/025-how-a-module-plugs-into-the-shell/02-how-a-module-plugs-in.md
T
jochen ac6c306df3 Research 025: how a module plugs into the operator's shell
Opened after the zsh module's rollout (to-be 38 WP5) was stopped: every machine
carries the same predecessor-written startup file, the module's block would
duplicate it and drop lines, nothing installs the prompt, and execute never
reads .zshrc. Weighs how modules contribute environment and shell code, where
the operator's own lines go, ordering, and what a contribution is addressed to.
2026-10-04 10:30:18 +02:00

11 KiB

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 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). 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.