12 KiB
layer, status, code, updated, decisions
| layer | status | code | updated | decisions | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| to-be | in-progress |
|
2026-10-04 |
|
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
usershape; - 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;
- one in
- contributes its own environment: the editor, the configuration home, and
~/.local/binplus the two script directories onPATH; - serves
executeand its ownzsh_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
PATHentry, 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.localalready.
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
usershape records, in its applied record, the login shell it found whenever it changes it. - Removing a
usernever 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
~/.configor~/.local/sharewould have been created as root's. - Giving the shell back is reported, never fatal. A failed
usermodon 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
userno 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
usermodruns; - 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) andnode-login-shell(node scope, the verbexecute). A module may no longer declare a seat namedlogin-shellornode-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, withvariablesandpath: a variable name must be a POSIX name and notPATH; a value may not contain$, a quote, a backslash or a line break; a path entry's place isstartorend;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,PATHentries 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
shleavesPATHunchanged; - 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 claimsnode-environmentand 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
.zshenvblock that sources the environment file. - Its
.zshrcblock goes at the start and carries today's shared defaults, with the three slots. - It keeps the
usershape. executerunszsh -lcin 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.
- It drops its seat declaration and claims
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-autosuggestionsandzsh-syntax-highlighting, new. Each declares its package and contributes its loader from the distribution's path, in thenormalandlastslots.
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
.zshrcblock with the prompt innormaland highlighting inlast; - the environment file with the shell's
PATHentries; - 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,enableanddisableescalate withsudo -nwhen 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
systemctlis 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.
statussays 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.unitsanswers in both scopes on a workstation and on a server;restartof a harmless unit answersok.
WP5 — Assign and prove
Operator-gated. Nothing here runs without the operator's go-ahead.
Order.
- Merge WP1 and roll the host.
- Merge WP2, and push the controller.
- Merge WP3 and WP4, and build the new modules by hand: a new catalogue module's first build is asked for, not automatic.
- On one server, assign
node-env,zsh,zsh-autosuggestionsandzsh-syntax-highlighting, and push. Then check:node-login-shell.execute@<server> command="echo $PATH"shows the shell's entries;.zshrcbegins with the block;- the operator's lines follow untouched;
- the environment file and the service manager's file exist.
- The operator deletes the duplicated lines, per the zsh module's documentation.
- The other server, then the two workstations, the workstations also with
powerlevel10k. - Assign
systemdeverywhere, and prove WP4. - 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.