239 lines
12 KiB
Markdown
239 lines
12 KiB
Markdown
---
|
||
layer: to-be
|
||
status: in-progress
|
||
code: [mesh-host, mesh-controller, mesh-catalog]
|
||
updated: 2026-10-04
|
||
decisions:
|
||
- 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](38-building-the-operators-machine.md) WP5, and finishes the service-manager module of WP6
|
||
short of its user-scoped units. The decisions are [ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md)
|
||
(the environment), [ADR 0204](../../02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md)
|
||
(shell code and the seat) and [ADR 0205](../../02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md)
|
||
(vendored software). The evidence is [research 025](../../01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md).
|
||
|
||
## 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](../../00-META/process/07-feature-branches.md)). 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](../../04-ISSUES/228-a-login-the-mesh-set-is-never-given-back/00-report.md).*
|
||
|
||
**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.
|