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

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's execute verb (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

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

References