Merge pull request 'Research 018 graduates: ADRs 0173–0177 and to-be 37, the operator's machine' (#293) from feat/the-operators-machine into main
This commit was merged in pull request #293.
This commit is contained in:
@@ -78,6 +78,8 @@ capability. The host hardcodes no firewall, supervisor, package manager or runti
|
||||
generic apply primitives and platform detection, so it runs where none of those exist — an Android
|
||||
phone has no ufw, systemd, pacman or Docker.
|
||||
|
||||
> **The mechanism changed — 2026-10-02, by [ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md).** The shell example above — *bash, zsh and fish all join `shell`; one may be default* — is read as *installed is not holding*: the three may all be installed, and the `login-shell` seat is node-scoped and held by exactly one. The decision — what a module is, and the three relationships — stands; [ADR 0173](0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md) applies it to the operator's whole machine.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Supersedes the earlier "grouped by domain" decision** (folded in consolidation; see the
|
||||
|
||||
@@ -9,6 +9,8 @@ extends: 0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md
|
||||
|
||||
# 47. A module runs its code as its own process, with its own account
|
||||
|
||||
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** A module's *tools* are no longer served by the module's own process under its own account: one tool runtime per node, on the host side, serves every assigned module's bundle. A tool is still served on its own subject and only the module that serves it answers; what moved is the process and the account.
|
||||
|
||||
## Context
|
||||
|
||||
A module is one self-contained thing ([ADR 0040](0040-what-a-module-is.md)), and it gets a broker
|
||||
|
||||
@@ -9,6 +9,8 @@ extends: 02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-ow
|
||||
|
||||
# 150. A module's own code runs as supervised processes under the module's one account
|
||||
|
||||
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** For a module's *tools*, read that record: one runtime per node, the node's one account, bundles loaded from the memberships. This record still governs a module's long-lived processes — a daemon, a provisioner, a scheduled ingest — and the account invariant for them.
|
||||
|
||||
## Context
|
||||
|
||||
The repository answers "what runs a module's own code" two ways and reconciles them nowhere
|
||||
|
||||
@@ -111,6 +111,8 @@ operator owns; narrowing what it may call is a setting on its assignment, which
|
||||
[ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) already provides for
|
||||
and nothing here builds.
|
||||
|
||||
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** The surface stays a module assigned per node, on loopback, with the machine's login as the authority. It is no longer a container: it is the node tools runtime's serving mode, host-side, and that runtime also serves every assigned module's tools. The module is renamed `node-tools`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **The way a person drives the mesh is inside the mesh.** It is declared, delivered, replaced and
|
||||
|
||||
@@ -94,6 +94,13 @@ itself restarts, and the console says so rather than hiding the modules' tools w
|
||||
**A grant of `*` reaches a role's tools; `seat:<seat>.<verb>` grants one.** The console's `*` needed no
|
||||
change to reach the mesh's verbs, which is what a grant meaning *every tool* should mean.
|
||||
|
||||
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).**
|
||||
> The seat gains a generic verb beside the named ones: `command`, which takes one command line as the
|
||||
> controller's binary takes it and answers what it printed. The named verbs stand and keep their
|
||||
> schemas; `command` is the whole binary, added because the operator decided any node may call any
|
||||
> tool and a verb per command was the only thing keeping `node account`, `node show` and the rest
|
||||
> behind a shell on the control node. Additive within the version, as §"additive" above allows.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **The console answers the mesh's own questions.** Issue 147's first paragraph closes: what a node
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-10-02
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0040-what-a-module-is.md
|
||||
---
|
||||
|
||||
# 173. The operator's machine is the mesh's, and a module is whatever it declares
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0040](0040-what-a-module-is.md) says a module is *one self-contained piece of software the
|
||||
mesh installs and manages*, and every example it gives is a service: a database, an analytics
|
||||
server, a forge. The catalogue followed the examples. Of the predecessor's 34 modules on one
|
||||
workstation, 28 are the operator's environment — a login manager, a window manager with 88 files
|
||||
and four flavors, a shell, a terminal, a launcher, an audio setup, scripts — and the migration
|
||||
scoped all 28 out as *the workstation's own environment*, to be managed by nobody
|
||||
([research 018](../01-RESEARCH/018-the-operators-machine-as-modules/02-what-exists-and-what-is-missing.md)).
|
||||
Since the predecessor retired, nobody is exactly who manages them: a fix is a hand edit that
|
||||
nothing records and nothing regenerates.
|
||||
|
||||
[To-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) reached under the home for
|
||||
one directory and drew a boundary inside it. The operator's statement is wider: *the mesh manages
|
||||
my entire machine, all four of them, as far as it makes sense* — system folders and the home
|
||||
alike, the servers and the workstations from the same catalogue. And the operator refused a
|
||||
distinction this effort first drew between modules that ship code and modules that ship only
|
||||
declarations: *a module can have some tools, a seat implementation, some containers, a unit, a
|
||||
binary, some config files — one of these, or all, or two.*
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Keep 0040's reading and manage the environment outside the catalogue** — dotfiles in a
|
||||
repository, a script that places them. Rejected: that is the predecessor's first two days, the
|
||||
origin of every inherited shape [as-is 10](../03-DESIGN/00-as-is/10-module-catalogue.md)
|
||||
documents, and it puts the one thing a person looks at outside the one mechanism that is
|
||||
checked.
|
||||
2. **Add a second kind of module for configuration** — a "config module" with files and no
|
||||
process. Rejected by the operator: a kind is a distinction the manifest already makes by what
|
||||
it declares, and a second kind is a second set of rules to keep in step.
|
||||
3. **One definition: a module is one managed thing, described by what it declares.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. Everything configurable on a node is declared by a module.** Services, and equally the login
|
||||
manager, the display server, the window manager, the shell, the terminal, the launcher, the
|
||||
notifier, the audio setup, the boot images, the package manager's configuration, the agent at the
|
||||
terminal, and a folder a person works in. The test is *can it be configured on a machine*; if it
|
||||
can, some module owns it. What no module declares is found and left alone, as adoption already
|
||||
says of a machine ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
|
||||
|
||||
**2. A module is whatever it declares, and there are no kinds of module.** A package, files, a
|
||||
container, a unit, a binary, a seat claim, tools — any one, or all. 0040's *one self-contained piece
|
||||
of software* stands; its examples were services, and that was the whole of the bias. A downloads
|
||||
folder with a process that tidies it, backs it up and answers questions about it is a piece of
|
||||
software by 0040's own test, and so is a shell that is a package, three files and a seat.
|
||||
|
||||
**3. The home has no boundary of its own.** A file under the operator's home is placed and owned
|
||||
the way [to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §2 built it: by a
|
||||
module, resolved against the account's home, owned by the account. Which files are the mesh's is
|
||||
decided by what modules declare, not by a line drawn through a directory. A person's documents,
|
||||
projects and history are data under [ADR 0051](0051-shared-data-is-the-operators.md) and no module
|
||||
declares them.
|
||||
|
||||
**4. One module ships one default configuration.** No flavors. What differed between the
|
||||
predecessor's four flavors of one desktop module is what [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)
|
||||
is for.
|
||||
|
||||
**5. Servers and workstations take the same catalogue.** A module declares what it needs; a
|
||||
machine reports what it has; assignment refuses by name
|
||||
([ADR 0161](0161-what-deserves-a-seat.md) §3). The shell, the prompt, git and the agent are universal.
|
||||
A display server needs a graphical session; a window manager needs the display server held. Nothing
|
||||
in a manifest says *workstation*.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The catalogue grows by a family of modules that run no service. Each is still built,
|
||||
registered, assigned, pushed and reported like every other, and `status` says whether a
|
||||
machine has applied them.
|
||||
- The account fact becomes load-bearing for every node a person uses. Today it is empty on all
|
||||
four node records of this mesh; stating it is the first step of the build.
|
||||
- A module that *installs* a thing is distinct from a module that *holds its role*: zsh, fish and
|
||||
bash may all be installed, and one holds the login shell
|
||||
([ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)).
|
||||
- The host's `package` shape drives the distribution's package manager only. A module whose
|
||||
package is outside the distribution's repositories — the login manager in use is one — needs
|
||||
either an official package or a shape the host does not have. Recorded as a gap, not decided.
|
||||
- The predecessor's hooks go. What they did becomes declared state the host applies, or a verb a
|
||||
seat serves ([ADR 0177](0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)).
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A manifest with no container, no unit and no binary registers and resolves like any other | the catalogue's registration tests, with a package-and-files manifest |
|
||||
| A file resource under the home resolves against the account and is owned by it | the controller's composition tests (to-be 29 §2, built) |
|
||||
| A home-scoped module is refused on a node with no account, naming the fact | the same tests |
|
||||
| A module needing a capability the machine lacks is refused by name | the resolver's tests (ADR 0161 §3) |
|
||||
|
||||
## References
|
||||
|
||||
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md), documents
|
||||
01 and 02 — the behaviour wanted and the inventory measured.
|
||||
- [ADR 0040](0040-what-a-module-is.md), [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md),
|
||||
[ADR 0051](0051-shared-data-is-the-operators.md), [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)
|
||||
- [To-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) — the account and the home
|
||||
as a placement root, built; the records for them are proposed in an open change.
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
---
|
||||
topic: building it
|
||||
status: accepted
|
||||
date: 2026-10-02
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0011-managed-files-are-generated-never-edited.md
|
||||
---
|
||||
|
||||
# 174. A node varies a module through settings and kept regions, never through an edit
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0011](0011-managed-files-are-generated-never-edited.md) says a managed file is derived and an
|
||||
edit to it is overwritten without warning. The predecessor said the same and then undid it twice:
|
||||
a `merge` strategy that adopted disk drift back into its database, so a local edit became the
|
||||
record; and a theming layer of about 90 environment variables substituted into templates at sync
|
||||
time, with tools to list and set them, so that *nearly every value was a variable* — a second
|
||||
configuration language laid over the first.
|
||||
|
||||
The operator wants both the variation and the rule. One window-manager module with one default
|
||||
configuration, and each node tweaking it; and the file carrying the wanted value rather than a
|
||||
variable the file reads. Two mechanisms already exist for exactly this: a **setting**, declared by
|
||||
the module and set per mesh or per node, rendered at composition
|
||||
(`${setting:…}` is live in the resolver's manifest); and a **kept region**, a block in a file the
|
||||
mesh writes *into* where the operator's own lines survive every push
|
||||
([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), used by the ssh-client module
|
||||
for the operator's own `Host` blocks).
|
||||
|
||||
What stands in the way is [issue 168](../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md):
|
||||
a setting today reaches every mergeable file and every contribution of its module. Ninety theme
|
||||
knobs on that mechanism would reach ninety files. The record that fixes it — a setting declared
|
||||
with its type, meaning, default and the file it lands in — is proposed in an open change alongside
|
||||
the container-runtime records.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Carry the predecessor's merge strategy.** A local edit is adopted into the node's layer.
|
||||
Rejected: two writers and no arbiter, which is the option 0011 removed, and the reason a
|
||||
`/model` choice was silently reverted on every node for weeks before anyone found the cause.
|
||||
2. **Carry the environment-variable theming.** Rejected by the operator: the value belongs in
|
||||
the file; a variable the file reads is a second place for the same fact.
|
||||
3. **A per-node file override** — a whole file replaced for one node. Rejected: it is a flavor
|
||||
under another name, and a module update then misses that node entirely.
|
||||
4. **Settings rendered into the file, and kept regions, and nothing else.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**A node varies a module in exactly two ways.**
|
||||
|
||||
- **A setting.** Declared by the module with a default, set for the mesh or for one node, rendered
|
||||
into the file at composition. The value is in the file. Asked, the mesh lists every setting
|
||||
with its effective value and where it came from.
|
||||
- **A kept region.** A marked block in a file the mesh writes into, in which the operator's own
|
||||
lines are kept across every push and given back when the module goes (ADR 0102).
|
||||
|
||||
**An edit outside a kept region is overwritten, as ADR 0011 says, and never adopted.** Nothing
|
||||
reads a managed file back into the record.
|
||||
|
||||
**The predecessor's theme knobs become settings** of the modules whose files they render — the
|
||||
window manager's colours are the window manager's settings, the bar's are the bar's — each
|
||||
landing in the file that reads it and no other.
|
||||
|
||||
**Issue 168 is fixed before any environment module declares a setting.** A setting must name the
|
||||
file it lands in; until that ships, the environment modules carry their defaults in their files
|
||||
and no settings.
|
||||
|
||||
## Consequences
|
||||
|
||||
- No flavors, no per-node file copies, no environment layer. A module's definition is one set of
|
||||
files; a node's difference is data in its layer, visible by asking.
|
||||
- The settings record proposed alongside the container-runtime records is on the critical path
|
||||
of every module with a knob, and this record depends on it shipping as proposed.
|
||||
- A kept region is the only place a person edits a managed file, and the file says where it is.
|
||||
The operator's own prompt customisations, aliases and window rules live there.
|
||||
- What got harder: a change that is neither a setting the module declared nor the operator's own
|
||||
lines has no home, and is refused by the mechanism rather than silently kept. That is the point.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A setting reaches only the file its declaration names | the controller's settings tests, once the proposed record ships; issue 168 closes on it |
|
||||
| A kept region survives a push with its content and is given back on undeclare | the host's write-into tests (ADR 0102), with a region declared by an environment module |
|
||||
| An edit outside a region does not survive a push | the same tests, asserting the file equals the composed content outside the region |
|
||||
| Every effective value names its source | `mesh-controller.settings` and the module's own `show-config` tool |
|
||||
|
||||
## References
|
||||
|
||||
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/01-the-intended-behaviour.md) §"One default, varied by settings, never by edits"
|
||||
- [ADR 0011](0011-managed-files-are-generated-never-edited.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md),
|
||||
[issue 168](../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-10-02
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
||||
---
|
||||
|
||||
# 175. One tool runtime per node serves every module's tools, on the host side
|
||||
|
||||
## Context
|
||||
|
||||
A module's tools are code the module wrote, one function behind each verb, served on the subjects
|
||||
the controller issues in the module's membership
|
||||
([ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)).
|
||||
What *runs* that code is [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md):
|
||||
a supervised process per module under the module's own account, and in the catalogue as built,
|
||||
that process is a container per module per node, built on the tool runtime's base image.
|
||||
|
||||
Measured on the live mesh ([research 018](../01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md)):
|
||||
67 module tools, each served from its module's container; the packet-filter seat's three verbs
|
||||
served by a container with `NET_ADMIN` on every one of four machines, for a module that is
|
||||
otherwise a package, three files and a service; and the console, a container per node, calling
|
||||
everything and serving nothing. The operator's environment adds a dozen modules of the
|
||||
packet-filter shape, and the operator's judgement is plain: *I would never run MCP tools inside
|
||||
a container; that is a very bad design.* And: *I don't care about permissions or account per
|
||||
module, that just complicates things for no good reason. Just a node-level tool executor. If a
|
||||
command needs root, that's the module's concern.*
|
||||
|
||||
The tool runtime itself was written for this. Its own description: *the per-node process that
|
||||
makes a module's tools actually serve — imports the assigned modules' compiled tool entrypoints,
|
||||
each of which registers its tools as it loads; on a node the host resolves the list and starts it
|
||||
like any other supervised workload.* What the catalogue did instead was build one image per module
|
||||
around it.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Keep a process per module.** Rejected: one container per module per node for software that
|
||||
is not a container, and the account-per-module invariant it exists to protect is one the
|
||||
operator declines to pay for.
|
||||
2. **The host executes tools itself.** Rejected: the host is a static Go binary that loads no
|
||||
plugins; a module's tools are TypeScript on the SDK, and building a second SDK in Go for the
|
||||
host's sake is the cost ADR 0039 refuses.
|
||||
3. **One tool runtime per node, a sibling of the host, loading every assigned module's bundle.**
|
||||
Chosen. It is what the runtime was written to be.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. One tool runtime per node, supervised by the host, on the host side — never a container.**
|
||||
The host starts it the way the launcher starts the host
|
||||
([ADR 0005](0005-the-node-host.md)): a process on the machine, restarted when it dies. It holds one
|
||||
bus credential, the node's. It is module-agnostic: it knows bundles and subjects, nothing of what
|
||||
any module does.
|
||||
|
||||
**2. It serves every assigned module's tools and every held seat's verbs** on the subjects the
|
||||
memberships issue. ADR 0159 and ADR 0160 are unchanged in what they say about subjects, grants
|
||||
and memberships; what changes is that one process on the node subscribes to all of them instead of
|
||||
one process per module. A module that runs a long-lived service of its own — a daemon, a
|
||||
container — keeps it; this record is about tools.
|
||||
|
||||
**3. A module brings its tools as a bundle**, the artifact kind the catalogue already has for
|
||||
interpreted code, built by the pipeline and delivered to the node by the host as it delivers any
|
||||
artifact. Never an image. The runtime loads each bundle as the membership names it, and a push
|
||||
that adds or replaces a bundle reaches a running runtime as a reload.
|
||||
|
||||
**4. Root is the module's concern.** A tool that must change the packet filter or rebuild boot
|
||||
images escalates itself. The runtime does not run as root for everyone's sake; the caller does not
|
||||
know and need not.
|
||||
|
||||
**5. Any node may call any tool on any node.** The runtime's credential may call everything, as
|
||||
the console's already may. A per-module calling grant is not kept.
|
||||
|
||||
**6. The console is this runtime's serving mode, renamed.** [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)
|
||||
stands in substance — a module assigned per node, MCP on the machine's loopback, the machine's
|
||||
login is the authority — and changes in form: host-side, serving as well as calling, and named for
|
||||
what it is: **node tools**. The mesh's own verbs stay with the controller
|
||||
([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)); a mesh-scoped seat's verbs
|
||||
run on the node that holds it ([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)).
|
||||
|
||||
**Where ADR 0047 and ADR 0150 say a module's tools are served by the module's own process under
|
||||
the module's own account, read this record.** Everything else they decided stands: a tool is served
|
||||
on its own subject, only the module that serves it answers, a module's long-lived processes are the
|
||||
machine's to supervise. The invariant 0150 kept — one account per module — no longer holds for
|
||||
tools, and the reason is stated above: every tool is callable from everywhere by decision 5, so the
|
||||
account no longer scopes anything a caller cannot already reach.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The packet-filter module's container goes; its verbs run on the host side and escalate as they
|
||||
need. [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md) §3's container capability is moot
|
||||
for it.
|
||||
- The tool runtime's base image stays the way a module's *service* may be built; it is no longer
|
||||
the way tools reach a node.
|
||||
- The node tools runtime needs an interpreter on the machine. The module that is the runtime
|
||||
declares it as a package.
|
||||
- The container-runtime seat proposed in an open change says its holder *runs as a supervised
|
||||
process and serves the verbs locally to the host and on the bus*. A supervised process serving
|
||||
verbs is what this runtime is; whether that holder keeps a process of its own or serves through
|
||||
the runtime is for that record's build to say.
|
||||
- What got harder: one process carries every module's tool code on a node, so one module's
|
||||
faulty bundle can take down the node's tools. The runtime loads each bundle guarded and names
|
||||
the one that failed; the others serve.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| The runtime loads every bundle its memberships name and serves each tool on its subject | the runtime's tests against a real bus: two bundles, three tools, each answers |
|
||||
| A bundle that fails to load is named and the others serve | the same tests, with one bundle that throws on load |
|
||||
| The host supervises the runtime and restarts it | the host's tests over the launcher's shape |
|
||||
| A push that replaces a bundle reloads it without a restart | the runtime's tests: a bundle replaced on disk, the membership re-read, the new tool answers |
|
||||
| No module in the catalogue declares a container whose only purpose is tools | a catalogue check: a manifest with `tools` and an image artifact built on the tool runtime's base is refused once the runtime is live |
|
||||
| Live | `login-shell.execute@<node>` answers on every node from the node tools runtime; `docker ps` shows no per-module tool container |
|
||||
|
||||
## References
|
||||
|
||||
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md)
|
||||
- [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md), [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md),
|
||||
[ADR 0152](0152-the-operators-surface-is-a-module-the-console.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md),
|
||||
[ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md), [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
|
||||
- [To-be 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md), [to-be 34](../03-DESIGN/01-to-be/34-the-console.md)
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-10-02
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||
---
|
||||
|
||||
# 176. The login shell is a node seat held by one shell module, and `execute` is its contract
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0040](0040-what-a-module-is.md) names the shell as its example of a *shared* seat: bash, zsh
|
||||
and fish all join `shell`, and one may be default. The operator's reading is sharper, and it
|
||||
matches [ADR 0126](0126-a-module-declares-its-own-seats.md) better: *installing* a shell is
|
||||
installing software, and several may be installed; *holding* the seat is being the login shell,
|
||||
which a node has exactly one of. A definition says which seats a module can hold; the assignment
|
||||
says which it does.
|
||||
|
||||
A seat carries the tools its holder must serve ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)),
|
||||
and [to-be 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) leaves which verbs each
|
||||
seat serves as a decision per seat, taken slowly. This is the first seat of the operator's
|
||||
environment, and the one every node has.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **A shared `shell` seat with a default**, as 0040's example reads. Rejected: *default* is a
|
||||
second concept beside *holder* for the same fact, and the `user` shape already makes the
|
||||
login shell declared state ([to-be 05](../03-DESIGN/01-to-be/05-the-node-host.md)).
|
||||
2. **No seat; each shell module sets the login shell for itself.** Rejected: two assigned shell
|
||||
modules would fight over `chsh`, and nothing would say which won.
|
||||
3. **An exclusive node-scoped seat, `login-shell`, declared by the shell modules, held by one
|
||||
per node.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. `login-shell` is a node-scoped seat declared by the shell modules.** zsh, fish and bash each
|
||||
declare that they can hold it; a node's assignment says which does; the controller refuses a
|
||||
second holder by name as for every seat. A shell module that is assigned without holding the seat
|
||||
is installed and nothing more.
|
||||
|
||||
**2. Holding the seat sets the account's login shell.** The holder's declaration carries the
|
||||
`user` shape with the shell it provides, so the login shell is declared state the host applies and
|
||||
gives back when the holding moves — `chsh` stops being a hook.
|
||||
|
||||
**3. The seat's contract is `execute`.** One verb, one argument, the command, run on the node the
|
||||
seat is scoped to as the operator account, answering with what it printed and how it exited.
|
||||
Every holder serves it; a holder may serve its own tools beside it
|
||||
([ADR 0170](0170-the-firewall-seat-serves-its-verbs.md) §2) — show the rendered configuration, list
|
||||
the plugins, set a prompt value.
|
||||
|
||||
**4. Any node may call it on any node.** The grant is the node tools runtime's
|
||||
([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) §5):
|
||||
*run `uptime` on every node* is five calls to one verb.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The first environment module is a shell: a package, files under the home owned by the account,
|
||||
a seat declaration and claim, a `user` shape, and one tool. It proves the whole pattern on every
|
||||
node, servers included, before anything graphical is written.
|
||||
- ADR 0040's shell example is read as *installed is not holding*; a dated note in that record says
|
||||
so. Its decision is untouched.
|
||||
- `execute` is a shell on every machine, addressed over the bus. That is the point, and it is
|
||||
the widest verb the mesh serves; it exists because the operator decided every node may call
|
||||
every tool, and this record does not narrow that.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| Two shell modules assigned to one node, one holding: one `user` shape in the declaration, naming the holder's shell | the controller's composition tests |
|
||||
| A second claimant is refused by name | the catalogue's seat tests |
|
||||
| `execute` runs as the account and answers output and exit status | the module's tool tests over a fake runner, and live on every node |
|
||||
| The seat's verb appears with its scope and machine in the node tools listing | the runtime's tests (to-be 33 §4) |
|
||||
|
||||
## References
|
||||
|
||||
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md)
|
||||
- [ADR 0040](0040-what-a-module-is.md), [ADR 0126](0126-a-module-declares-its-own-seats.md),
|
||||
[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-10-02
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
|
||||
---
|
||||
|
||||
# 177. A unit may be user-scoped, and the service manager is a node seat whose holder answers for the units
|
||||
|
||||
## Context
|
||||
|
||||
The host's `service` shape puts a system unit into a state. It has no user scope.
|
||||
[To-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) states the gap: *a
|
||||
workstation's per-user daemons have no form the mesh can send.* Four of the predecessor's
|
||||
environment modules ship user units — the desktop's reload watcher and bar watchdog, the audio
|
||||
module's masks, the power module's memory guard, the thermal daemon's profile switcher — and the
|
||||
predecessor needed a hook to enable them because *shipping a unit file does not run it*; one unit
|
||||
was deployed for months and ran on one machine only.
|
||||
|
||||
[ADR 0040](0040-what-a-module-is.md) says the host hardcodes no supervisor, and a swappable
|
||||
machine mechanism is a module implementing a capability — which is what the nftables module is for
|
||||
the packet filter ([ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)). The service manager is
|
||||
reported today as a capability, `service-manager`, and held by nobody. The operator's proposal: a
|
||||
systemd module that holds the seat and serves the tools about units, system and user.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Keep user units as a module concern** — each module runs `systemctl --user` in a hook.
|
||||
Rejected: that is the hook that silently never ran, and an action over the link is refused.
|
||||
2. **The service-manager module applies units** on behalf of others, as a provision. Rejected by
|
||||
the operator: provisioning is for resources a provider creates for a consumer; a unit is
|
||||
declared state the host applies, as every resource is.
|
||||
3. **The host's `service` shape gains a user scope; a systemd module holds the service-manager
|
||||
seat and serves the verbs about units.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. The `service` shape gains `scope`: `system` (the default) or `user`.** A user-scoped unit
|
||||
is applied as the operator account through the account's own service manager: enabled, started,
|
||||
stopped, reloaded on its triggers, exactly as a system unit is, and refused on a node with no
|
||||
account, naming the fact. The host applies it; no module does.
|
||||
|
||||
**2. `node-service-manager` is a seat of the mesh's own, node-scoped**, seeded by the controller
|
||||
under this record, as ADR 0121 requires of a `node-*` name. The `systemd` module claims it and is
|
||||
assigned to every machine whose profile reports `service-manager`.
|
||||
|
||||
**3. The seat's verbs answer for every unit on the machine**, each taking an optional `scope`:
|
||||
`units`, `status`, `start`, `stop`, `restart`, `enable`, `disable`, `journal`. The host applies what
|
||||
is declared; the holder answers questions and operator acts about it, and says, for a mesh-held
|
||||
unit, that the host will restore what its declaration says.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The host's vocabulary grows by one field on one shape, asserted by its count test
|
||||
([to-be 05](../03-DESIGN/01-to-be/05-the-node-host.md)); an older host refuses a declaration
|
||||
carrying it, so the host rolls before the first module that uses it.
|
||||
- The predecessor's four user-unit modules become declarable without a hook.
|
||||
- The seat's holder is the first system seat held by a module that runs nothing of its own: its
|
||||
verbs are served by the node tools runtime ([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
|
||||
- What got harder: `journal` and `status` on a user unit need the account's manager reachable
|
||||
from the runtime's process, which runs as the node's account; the holder's tool escalates or
|
||||
switches user as it needs, which is ADR 0175 §4 applied.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A `service` with `scope: user` is enabled and started under the account, and refused with no account | the host's tests with a fake service manager |
|
||||
| The seat declares its verbs; a claim serving fewer is refused by name | the catalogue's seat tests |
|
||||
| The verbs act on a named unit in the named scope and name the unit's holder when the mesh declares it | the module's tests over a fake runner |
|
||||
| Live | the desktop's reload watcher declared `scope: user` on a workstation; `node-service-manager.status@<node>` reports it active |
|
||||
|
||||
## References
|
||||
|
||||
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md)
|
||||
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md),
|
||||
[ADR 0040](0040-what-a-module-is.md), [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)
|
||||
- [To-be 05](../03-DESIGN/01-to-be/05-the-node-host.md), [to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md)
|
||||
@@ -273,6 +273,10 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0150** — [A module's own code runs as supervised processes under the module's one account](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)
|
||||
- **0152** — [The operator's surface is a module the mesh assigns: the console](0152-the-operators-surface-is-a-module-the-console.md)
|
||||
- **0155** — [A definition names no installation: how that is checked, and the three ways a value that did gets out](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
|
||||
- **0173** — [The operator's machine is the mesh's, and a module is whatever it declares](0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md)
|
||||
- **0175** — [One tool runtime per node serves every module's tools, on the host side](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)
|
||||
- **0176** — [The login shell is a node seat held by one shell module, and `execute` is its contract](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)
|
||||
- **0177** — [A unit may be user-scoped, and the service manager is a node seat whose holder answers for the units](0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)
|
||||
|
||||
### How it is built
|
||||
|
||||
@@ -294,6 +298,7 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0107** — [Persistent data is a directory bind, never a named volume](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)
|
||||
- **0111** — [A build source is on the mesh's git seat, or it is an external repository](0111-a-build-source-is-on-the-git-seat-or-external.md)
|
||||
- **0149** — [The live mesh is the test bed](0149-the-live-mesh-is-the-test-bed.md)
|
||||
- **0174** — [A node varies a module through settings and kept regions, never through an edit](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)
|
||||
|
||||
### How it is checked
|
||||
|
||||
|
||||
Reference in New Issue
Block a user