Files
hq/02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.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
7.7 KiB
Markdown

---
topic: what runs on it
status: accepted
date: 2026-10-04
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
---
# 203. The account's environment is one module's, and every module contributes to it
## Context
A variable or a `PATH` entry is a fact about the operator's account. A toolchain needs its directory
on `PATH`, a version manager needs a variable naming its directory, an agent needs a variable that
turns one of its behaviours off, and the shell sets an editor. Today every one of these is a line of
one shell's syntax in one hand-written startup file. [Research 025](../01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md)
measured on four machines:
- about a quarter of the 65 lines a workstation runs at shell start are environment;
- written into `.zshrc`, that environment reaches only interactive zsh. It misses the login shell's
`execute` verb ([ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)),
every script, and every program a graphical session starts;
- the service manager's place for the account's environment, `~/.config/environment.d/`, holds
nothing on any machine.
The shell module as first written carried some of these lines in its own block and dropped the rest.
## Considered Options
1. **Each module writes its own lines into the shell's startup file.** Rejected: one shell's syntax,
read by one kind of start, and the same lines rewritten by every shell module.
2. **Contribute the facts to the login shell, whose holder renders them.** Rejected: the environment
then depends on which module holds the shell, every shell module renders the same facts again, and
the graphical session sees nothing.
3. **Option 2, and the service manager's holder renders the same facts a second time** into
`environment.d`. Rejected: one fact set with two owners, whose renderings can disagree, and a
duty for the service manager unrelated to managing services.
4. **One file in `environment.d` syntax, sourced by shells.** Rejected: that syntax is close to
POSIX assignment but not equal, and a value one reader accepts breaks the other.
5. **Shells read the service manager's environment generator.** Rejected: every shell start then
runs a process and depends on the service manager, and the output is unquoted.
6. **A module of its own holds the environment.** One mesh seat, held by one module per node,
whose files are the account's environment. Every module contributes facts to it, and those facts
are written in each reader's format. Chosen. It was the operator's proposal.
Within option 6, two ways to write the files:
- **a. The holder's own code renders what it receives.** This was research 025's starting position.
Rejected: the code needs something to run it whenever a contribution changes, and the result exists
only after a machine has applied and run it.
- **b. The controller renders the facts into the holder's files,** in two named formats, at
composition. Chosen. The result is in the declaration before any machine applies it, nothing has to
trigger anything, and the two formats are standards: POSIX shell assignment and the service
manager's `environment.d`. The controller learns no shell. It writes an assignment in a standard
syntax, as it already writes a fail2ban stanza a module supplied.
## Decision
**1. The environment is a node seat, `node-environment`, in the mesh's own set.** One module per node
holds it, and it is the only writer of the account's environment. The first holder is a module of its
own (working name `node-env`), with no package and no process.
**2. Any module contributes to it with `environment`:**
- **`variables`:** names and values. A name is a POSIX variable name and never `PATH`. A value is
literal; it may use `${machine:…}`, resolved first, and may not contain `$`, a quote, a backslash or
a line break. The only expansion is the mesh's own, so the two formats cannot read one value
differently.
- **`path`:** entries, each placed at the `start` or the `end` of the account's `PATH`.
**3. The holder places the rendered environment with two placeholders** in its own files:
- **`${environment:posix}`** renders lines a POSIX shell sources:
- every variable exported;
- every `PATH` entry added only if missing, so sourcing twice changes nothing.
- **`${environment:systemd}`** renders the same facts as the service manager's user environment, with
the account's existing `PATH` kept between the start and the end entries.
Each rendered line names the module that contributed it, so the file answers *where did this come
from*. Contributions are ordered by module name, and then in the order a module declared them.
**4. The seat's protocol fixes where the POSIX file is:** `~/.config/mesh/environment.sh` under the
account's home. A shell sources that path without knowing which module wrote it. The service
manager's file is `~/.config/environment.d/50-mesh.conf`.
**5. Refused at composition:**
- two modules on one node setting the same variable, both named;
- an environment placeholder in a module that does not claim `node-environment`.
A node with contributions and no holder writes them nowhere. The holder's absence is visible in the
node's assignments, and no contributor is refused for it, because a missing `PATH` entry is a gap, not
a broken machine.
## Consequences
- A shell's part in the environment is one line in its always-read startup file, sourcing the
POSIX file. A second shell module writes the same line in its own syntax, and no contributor
changes when the login shell does.
- The graphical session sees the same `PATH` as the terminal, from the same facts.
- The controller gains one gathered field and two renderers. Both are tested byte for byte, like
the jails a node composes ([to-be 31](../03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md)).
- What a person sets for themselves stays theirs: variables of their own sit in their own lines of
their shell's file, read after the mesh's.
- **What got harder:** a value that needs another variable expanded (`$HOME`, `$XDG_CONFIG_HOME`)
must be written with the mesh's own `${machine:…}` facts, or it is refused. Expansion at shell start
is exactly what made one value mean two things in two readers.
- Once [issue 168](../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)
closes, a value a person varies becomes a setting of the module that contributes it
([ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)).
## How it is checked
| Rule | Checked by |
|---|---|
| Both renderings, byte for byte, from a fixed set of contributions | the controller's environment tests |
| Sourcing the POSIX rendering twice leaves `PATH` unchanged | the same tests, running `sh` over the rendering |
| A variable set by two modules is refused, naming both | the controller's resolve test |
| An environment placeholder outside the holder is refused | the catalogue check, which registration runs |
| A value with `$`, a quote, a backslash or a line break is refused | the manifest's parse test |
| The zsh holder sources the path the seat fixes | the catalogue's zsh test |
## 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 0177](0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md),
[ADR 0182](0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
- [To-be 41](../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md)