Files
hq/02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md
T
jochen 0bf70ee8b4 Graduate research 025: the environment and the shell's contributions
ADR 0203: the account's environment is one module's (seat node-environment);
every module contributes variables and PATH entries, rendered by the
controller as a POSIX file and as environment.d.
ADR 0204: shell code is contributed to the login shell in named slots, and
login-shell becomes the mesh's node-login-shell.
ADR 0205: software the distribution does not package ships as a pinned
archive of the module.
Issue 225: undeclaring a user stops a node applying; the shell is never
given back or checked.
To-be 41 carries the work packages; to-be 38 WP5 points to it.
2026-10-04 10:30:23 +02:00

132 lines
6.7 KiB
Markdown

---
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:<shell>:<slot>}`. 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)