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.
7.7 KiB
topic, status, date, deciders, reconstructed, extends
| topic | status | date | deciders | reconstructed | extends |
|---|---|---|---|---|---|
| what runs on it | accepted | 2026-10-04 | jochen | false | 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
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'sexecuteverb (ADR 0176), 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
- 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.
- 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.
- 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. - One file in
environment.dsyntax, sourced by shells. Rejected: that syntax is close to POSIX assignment but not equal, and a value one reader accepts breaks the other. - 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.
- 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 neverPATH. 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 thestartor theendof the account'sPATH.
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
PATHentry 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 existingPATHkept 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
PATHas 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).
- 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 closes, a value a person varies becomes a setting of the module that contributes it (ADR 0174).
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 |