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