Files
hq/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md
T

12 KiB
Raw Blame History

layer, status, code, updated, decisions
layer status code updated decisions
to-be in-progress
mesh-host
mesh-controller
mesh-catalog
2026-10-04
02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md
02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md
02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md
02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md
02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md

41. The shell and the account's environment

What it takes for the operator's shell to be modules, without a machine losing anything it does today. This design replaces the shell half of to-be 38 WP5, and finishes the service-manager module of WP6 short of its user-scoped units. The decisions are ADR 0203 (the environment), ADR 0204 (shell code and the seat) and ADR 0205 (vendored software). The evidence is research 025.

What a node with a shell looks like

 toolchain, agent, …             prompt, plugins, version manager
        │ environment                     │ shell (zsh, slot)
        ▼                                 ▼
 node-environment  ── holds ──  node-env           node-login-shell ── holds ── zsh
        │                                                 │
        ├─▶ ~/.config/mesh/environment.sh  ◀─ sourced from zsh's block in ~/.zshenv
        └─▶ ~/.config/environment.d/50-mesh.conf  ◀─ read by the account's service manager
                                                          │
                                       ~/.zshrc: the mesh's block FIRST — defaults and the
                                       three slots — then the operator's own lines, kept

The environment module (node-env) holds node-environment. It has no package and no process. Its two files are written by the host from placeholders the controller fills.

The shell module (zsh) holds node-login-shell. It:

  • installs the package;
  • sets the login shell through the user shape;
  • writes two blocks:
    • one in .zshenv, sourcing the environment;
    • one at the start of .zshrc, holding the defaults every machine shares today: the title, the keybindings, the aliases and the two small functions, with the three slots in place;
  • contributes its own environment: the editor, the configuration home, and ~/.local/bin plus the two script directories on PATH;
  • serves execute and its own zsh_config.

The prompt module (powerlevel10k) ships the theme as a pinned vendored archive, and the prompt's configuration as its own file in a directory it owns. It contributes the zsh code that loads both.

Two plugin modules (zsh-autosuggestions, zsh-syntax-highlighting) each install their distribution package and contribute one line. Syntax highlighting goes in the last slot, which is what its upstream asks for.

What stays the operator's is everything below the block in .zshrc, and ~/.zshrc.local, which the operator's lines source as they do today. On every machine today that means:

  • the version manager's lines and the toolchain's PATH entry, until those modules exist;
  • the two variables naming the operator's own script library;
  • the agent's title variable;
  • the port aliases;
  • the workstation's desktop variables, which live in ~/.zshrc.local already.

Nothing is lost at any step, because a line moves out of the operator's part only when a module carries it.

The migration is a person's act, listed in the zsh module's documentation (ADR 0182): after the first push, delete from .zshrc the lines the block now carries. Until then they run twice, which is harmless and visible.

Work packages

WP1  the host gives a login back                 (mesh-host)            issue 228
WP2  the controller composes environment and shell code (mesh-controller)
WP3  the modules                                 (mesh-catalog)         needs WP2 to resolve
WP4  the service manager's module, finished      (mesh-catalog)         independent
WP5  assign and prove                            (operator-gated)       needs WP1–WP3 merged and rolled

WP1, WP2 and WP4 are independent, and are built in parallel on one feature branch per repository (playbook 07). WP3 is written in parallel and proven against WP2's controller before anything is published.

WP1 — The host gives a login back

mesh-host. Half a day. Issue 228.

What changes.

  • The user shape records, in its applied record, the login shell it found whenever it changes it.
  • Removing a user never deletes the account, whether or not the mesh created it. If the account's shell is still the one the mesh set, and the recorded shell is still executable, the recorded shell is set back. Otherwise the shell is left as it is, and the outcome says why.
  • Before a shell is set, it is refused unless it is executable and listed among the machine's shells. The exception is a shell that refuses logins (nologin, false): the distribution does not list those, and the controller's own account uses one, so it need only be executable. The refusal fails that resource and leaves the account untouched.
  • A directory the host creates on the way to a file, a block or an archive inside an account's home belongs to that account, the home itself included when the host makes it. A directory that was already there keeps its owner and mode (ADR 0182). Until this, a fresh account's ~/.config or ~/.local/share would have been created as root's.
  • Giving the shell back is reported, never fatal. A failed usermod on removal is named in the outcome and the record is dropped, because a fatal removal is exactly the wedge issue 228 is about.

Proof. The host's tests:

  • an undeclared user no longer stops the apply;
  • the found shell comes back;
  • a shell changed by a person since is left alone;
  • a missing shell is refused before usermod runs;
  • a created account survives its removal.

WP2 — The controller composes environment and shell code

mesh-controller. One to two days.

What changes.

  • The seat table. It gains node-environment (node scope, no verbs) and node-login-shell (node scope, the verb execute). A module may no longer declare a seat named login-shell or node-login-shell. Both new seats are seeded into a live store by the existing additive seeding.
  • The manifest. It gains two contribution fields, each refused at parse when malformed:
    • environment, with variables and path: a variable name must be a POSIX name and not PATH; a value may not contain $, a quote, a backslash or a line break; a path entry's place is start or end;
    • shell: each entry names a known shell, a known slot, and non-empty code.
  • Composition. It fills ${environment:posix}, ${environment:systemd} and ${shell:<shell>:<slot>} in the claiming holder's file contents, from every module assigned to the node. The rendering is ADR 0203's and ADR 0204's: module order, a naming line per contribution, PATH entries added only when missing. ${machine:…} in a contributed value is resolved first.
  • Refusals. A variable set by two modules on one node is refused, naming both. A placeholder in a module that does not claim the matching seat is refused, both at the catalogue check and at composition.

Proof. The controller's tests:

  • both environment renderings, byte for byte, from a fixed set of contributions;
  • the POSIX rendering sourced twice by sh leaves PATH unchanged;
  • slot order and per-shell filtering;
  • each refusal, by name;
  • the seat table carries both seats and refuses a module declaring either.

The catalogue check over the whole catalogue passes.

WP3 — The modules

mesh-catalog. One day.

What changes.

  • node-env, new. It claims node-environment and declares two owned files: the POSIX file at the path the seat fixes, and the service manager's file, each holding its placeholder. It declares no tools.
  • zsh, rewritten.
    • It drops its seat declaration and claims node-login-shell.
    • Its environment moves to a contribution.
    • It writes a .zshenv block that sources the environment file.
    • Its .zshrc block goes at the start and carries today's shared defaults, with the three slots.
    • It keeps the user shape.
    • execute runs zsh -lc in the account's home, with the runtime's session words for the user manager. Its timeout is bounded below the runtime's thirty-second call limit; its output is cut at a bound and marked as cut; on timeout it kills the process group.
    • Tests cover the tool over real child processes and the manifest's shape.
    • Its documentation lists the one-off migration.
  • powerlevel10k, new.
    • The theme is vendored at a pinned upstream release, with its licence, as an archive artifact unpacked into the module's directory under the account's home.
    • The prompt configuration is today's file, as its own owned file in the same directory.
    • It contributes the zsh code that loads the theme and the configuration.
    • Today's file has the instant-prompt cache commented out, so the module does not turn it on.
  • zsh-autosuggestions and zsh-syntax-highlighting, new. Each declares its package and contributes its loader from the distribution's path, in the normal and last slots.

Proof. The controller's catalogue check over the whole catalogue passes. The modules' tests pass. A rehearsal composition for a node holding all five shows:

  • the .zshrc block with the prompt in normal and highlighting in last;
  • the environment file with the shell's PATH entries;
  • the service manager's file.

WP4 — The service manager's module, finished

mesh-catalog. Half a day. From the review of 2026-10-04.

What changes.

  • System-scope start, stop, restart, enable and disable escalate with sudo -n when the runtime is not root, as the packet filter and intrusion modules do. They name a refusal by how it failed.
  • User scope reaches the account's manager by its runtime directory, which the runtime's environment lacks.
  • A failed systemctl is an error, not an empty list.
  • The package resource goes: the service manager is always present, and it collided with the network module's identical declaration on a machine running both.
  • status says whether the mesh declares the unit. The restore note is attached only to such a unit.
  • Tests cover a fake runner.

The user-scoped units of mesh-host #72 stay to-be 38's WP6.

Proof. The module's tests. Live, after WP5:

  • node-service-manager.units answers in both scopes on a workstation and on a server;
  • restart of a harmless unit answers ok.

WP5 — Assign and prove

Operator-gated. Nothing here runs without the operator's go-ahead.

Order.

  1. Merge WP1 and roll the host.
  2. Merge WP2, and push the controller.
  3. Merge WP3 and WP4, and build the new modules by hand: a new catalogue module's first build is asked for, not automatic.
  4. On one server, assign node-env, zsh, zsh-autosuggestions and zsh-syntax-highlighting, and push. Then check:
    • node-login-shell.execute@<server> command="echo $PATH" shows the shell's entries;
    • .zshrc begins with the block;
    • the operator's lines follow untouched;
    • the environment file and the service manager's file exist.
  5. The operator deletes the duplicated lines, per the zsh module's documentation.
  6. The other server, then the two workstations, the workstations also with powerlevel10k.
  7. Assign systemd everywhere, and prove WP4.
  8. Unassign one plugin module on one machine. Its line leaves the block at the next push, and nothing else changes.

The follow-up records to-be 38 names are still owed:

  • what a shell module assigned beside the holder does;
  • how a person's own environment variable is a setting rather than a line, once issue 168 closes.