Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab1bd5598e | ||
|
|
3f3fb99219 | ||
|
|
3afe619531 | ||
|
|
502cf4839b | ||
|
|
0ba68c154e | ||
|
|
460793af1c | ||
|
|
25de331e9b | ||
|
|
ed5ddcdef6 | ||
|
|
e4f80cc3ce | ||
|
|
d2689c0f86 | ||
|
|
719aa6bd62 | ||
|
|
ca13f59c88 | ||
|
|
f8a0402485 | ||
|
|
9016d88d54 | ||
|
|
6e5dfd2ab8 | ||
|
|
872f20d51f | ||
|
|
550453c5db | ||
|
|
b7aebedc2d | ||
|
|
27b2d30441 | ||
|
|
82fa5f79ea | ||
|
|
f6668d76d6 | ||
|
|
f5d54db7aa | ||
|
|
bcf010886d | ||
|
|
2eba399e1e | ||
|
|
d227ed12d2 | ||
|
|
8712d666bf | ||
|
|
61e70b9395 | ||
|
|
de032e704c | ||
|
|
0c2eae07c5 | ||
|
|
f23a71e0d7 | ||
|
|
9c13c89fa3 | ||
|
|
2db0ea268d | ||
|
|
8d83d94659 | ||
|
|
a8ffc2b94b | ||
|
|
1dcbdae1c4 | ||
|
|
c3ec48f85c | ||
|
|
72eda923c7 | ||
|
|
c4fedcdbe3 | ||
|
|
0bf70ee8b4 | ||
|
|
947b85af5e | ||
|
|
ac6c306df3 | ||
|
|
96df3ccc88 | ||
|
|
bc64c5c187 | ||
|
|
be4b5777b8 | ||
|
|
d882b3568c | ||
|
|
a3523617d3 | ||
|
|
6b4da63261 | ||
|
|
e1b0bbde91 |
@@ -78,6 +78,12 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
|
||||
mesh-scoped exclusive claim is how the mesh says "there is one of me". A foundation seat is
|
||||
named after the server it guards: the `mesh-controller`, `postgres` and `lavinmq` modules claim
|
||||
the `mesh-controller`, `mesh-store` and `mesh-broker` seats ([ADR 0079](../02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md)).
|
||||
- **depends on a seat** — a module needing a seat held on its node by some module, without holding
|
||||
it. Derived from the resources it declares, never stated: a `service` depends on
|
||||
`node-service-manager`, a `package` on `node-package-manager`, a `container` on
|
||||
`node-container-runtime` ([ADR 0207](../02-DECISIONS/0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md)).
|
||||
Not a claim: a module **claims** a seat it holds and **declares** resources. Nothing claims a
|
||||
package.
|
||||
- **provision** — a service one module `provides` and others `require`; the mesh resolves a provider
|
||||
and wires the two with an endpoint and a credential. A provision is a service you offer, a seat
|
||||
is a role you occupy, and the two meet where a seat delivers a provision: occupying the seat is
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
status: graduated
|
||||
initiated: 2026-10-04
|
||||
touches: [the bus, what a module declares, the tool runtime, the SDK, the bus grants, 03-DESIGN/01-to-be/25-the-bus-on-nats.md, 03-DESIGN/01-to-be/32-what-a-module-declares.md]
|
||||
became: [02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md, 03-DESIGN/01-to-be/32-what-a-module-declares.md, 03-DESIGN/01-to-be/25-the-bus-on-nats.md]
|
||||
---
|
||||
|
||||
# 024 — State a module keeps on the bus
|
||||
|
||||
## What is investigated
|
||||
|
||||
A place on the bus where a module's own code keeps **current state** — not history — that every
|
||||
machine sees, including a machine that joins after the state was written: put, get, delete, list and
|
||||
watch, reached through the node's runtime the way a bundle already publishes, asks and subscribes
|
||||
([ADR 0198](../../02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)).
|
||||
On NATS that is a key-value bucket. The questions are what a module declares, who creates the
|
||||
bucket, what the grants are, what the runtime's verbs are, and what may never be stored.
|
||||
|
||||
## Why
|
||||
|
||||
The mesh carries two kinds of module traffic and a third is missing.
|
||||
|
||||
- **Events** land in the EVENTS stream: limits retention, seven days, ten thousand messages per
|
||||
subject, a durable consumer per consuming module that replays what it missed. Never a secret
|
||||
([design 32](../../03-DESIGN/01-to-be/32-what-a-module-declares.md) §10).
|
||||
- **Requests** are core request/reply — tool calls, a bundle's `mesh/ask` — and are kept nowhere.
|
||||
|
||||
Neither is *the current value of something*. Two cases from the first module that needs it, the
|
||||
operator's agent on a machine ([design 36](../../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md)):
|
||||
|
||||
1. **An MCP server registered for every machine.** Registering emits an event every machine's copy
|
||||
of the module consumes. A machine the module is assigned to *after* the registration has no
|
||||
durable consumer yet — the consumer is created at assignment — so it never hears of it. Wanted
|
||||
instead: one entry per server, for every machine or for one; every machine reads the whole current
|
||||
set when it starts and watches for changes; unregistering is a delete; any machine can list it.
|
||||
2. **Which licence a machine is bound to** ([design 39](../../03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md)).
|
||||
As events, a machine that was off for a day replays every rotation since and asks for a token
|
||||
after each. It needs only the latest binding and its generation. The token itself stays on
|
||||
request/reply and is never stored.
|
||||
|
||||
The design already expects this. [Design 25](../../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §1:
|
||||
"conditions and observed state in key-value buckets that anything may watch".
|
||||
[Research 017](../017-a-mesh-that-heals-itself/01-the-intended-behaviour.md) wants a provisioner's
|
||||
"what I applied" and a rotation's step kept in one rather than in memory. Nothing implements it.
|
||||
|
||||
## What exists, measured 2026-10-04
|
||||
|
||||
| | fact | where |
|
||||
|---|---|---|
|
||||
| streams | five kinds of mesh stream: CONTROL (work queue), NODES and ASSIGNMENTS (last per subject), EVENTS (limits: 7 days, 10 000 per subject), one work queue per seat that accepts | the controller's broker streams |
|
||||
| the state relationship | [design 32](../../03-DESIGN/01-to-be/32-what-a-module-declares.md) §4 already names *state* — 1:1, last per subject — and says it is "declared: the mesh's own". Two streams use it, both written by the controller. No module can declare it | design 32, the controller |
|
||||
| key-value buckets | none, anywhere | all four code repositories |
|
||||
| the runtime's bus verbs | `mesh/publish`, `mesh/ask`, `mesh/subscribe`; delivery back to the bundle is `mesh/event` | the runtime's launcher |
|
||||
| the runtime's principal | one bus user per machine carries every assigned module; its grant is the union of theirs. That one module's code does not act as another is the runtime's to keep: it publishes under the module's own name by construction | the controller's grant composition, the runtime's bus |
|
||||
| what a bundle is issued | a membership per assignment, last per subject, read directly by the runtime: where it serves, where it emits, what it reaches | ADR 0160 |
|
||||
| who creates bus objects | the controller only — mesh streams on every raise, a seat's stream at registration, a module's consumer at assignment. No module reaches the JetStream API | design 25 §3 |
|
||||
|
||||
### What a key-value bucket needs from a grant, against a real server
|
||||
|
||||
Measured against nats-server 2.10 with the Go client the runtime already uses, a bucket created by
|
||||
an unrestricted user and used by two users holding only the subjects below (`B` is the bucket):
|
||||
|
||||
| operation | subject published | writer | reader |
|
||||
|---|---|---|---|
|
||||
| bind to the bucket | `$JS.API.STREAM.INFO.KV_B` | yes | yes |
|
||||
| get | `$JS.API.DIRECT.GET.KV_B.>` | yes | yes |
|
||||
| put, delete | `$KV.B.>` | yes | **refused** |
|
||||
| list keys, watch | `$JS.API.CONSUMER.CREATE.KV_B.>` — an ordered, ephemeral consumer | yes | yes |
|
||||
| stop a watch cleanly | `$JS.API.CONSUMER.DELETE.KV_B.>` | yes | yes |
|
||||
| answers | its own inbox, which every principal already subscribes | — | — |
|
||||
|
||||
*Checked again once built, 2026-10-04:* the grants the controller composes for two machines' runtimes —
|
||||
one carrying the owner, one only a reader — were loaded into a server as composed, and each operation
|
||||
was run as each runtime's user. The owner's did all of them; the reader's read, listed and watched,
|
||||
and its put and delete were refused by the server.
|
||||
|
||||
Three things the measurement showed that reading the documentation would not have:
|
||||
|
||||
1. **A refused put is not an error to the caller; it is a timeout.** The server reports the
|
||||
permission violation asynchronously, on the connection, and the client waits out its deadline
|
||||
for an acknowledgement that never comes. So a runtime that relies on the grant alone tells a
|
||||
bundle "timed out" for "you may not write this" — it must refuse first, from what the module was
|
||||
issued, with the reason.
|
||||
2. **A watch's current values include deletions.** A key deleted earlier arrives among the initial
|
||||
values as a delete marker, before the end-of-current marker. A bundle asking "what is there now"
|
||||
must not be handed those.
|
||||
3. **Without the consumer-delete grant, stopping a watch hangs** until its deadline, and the
|
||||
ephemeral consumer lingers on the server until it times out by itself.
|
||||
|
||||
### Whether the events shape is enough instead
|
||||
|
||||
Honestly compared, because a new primitive is a cost:
|
||||
|
||||
- **EVENTS cannot be made last-per-subject for some subjects.** Retention is per stream, and
|
||||
JetStream refuses a second stream overlapping the first (verified and recorded in design 32 §3).
|
||||
A state subject inside `mesh.mod.*.event.>` keeps EVENTS' seven days: a licence binding unchanged
|
||||
for a week disappears.
|
||||
- **A separate last-per-subject stream per module** is possible — it is exactly what a key-value
|
||||
bucket *is* on the server: a stream with one message per subject, a rollup for purge, and direct
|
||||
reads. Building it by hand gives up the client's get, list, delete and watch, which are the
|
||||
operations both cases need, and would be the mesh writing NATS's own key-value layer again.
|
||||
- **Consumers are the wrong reader.** A durable consumer per reading module is created at
|
||||
assignment and replays from where it is; state wants "everything current, now, then changes",
|
||||
which an ordered ephemeral consumer from the last value per subject gives and a durable does not.
|
||||
|
||||
So key-value is not a convenience over events; it is the state relationship design 32 already
|
||||
names, opened to modules.
|
||||
|
||||
## Questions, and what this effort proposes
|
||||
|
||||
1. **What a manifest says.** `state` names the buckets a module owns, by local name — every
|
||||
instance of the module may write them and read them. `reads` names another module's bucket as
|
||||
`<module>.<name>`, read-only. Names only, never a bucket or subject (design 32 §1). A bucket's
|
||||
options — how many past values it keeps, how long a value lives — are the owner's to declare,
|
||||
the way a seat declares its own retention (design 32 §3).
|
||||
2. **Scope.** One bucket per module per name, mesh-wide. A key may carry a machine by the module's
|
||||
own convention (`all.<server>`, `<machine>.<server>`). A bucket per machine was considered and
|
||||
not proposed: "list every server for every machine" becomes a walk over buckets, and the grant
|
||||
could only narrow writes, which nothing asked for — every instance of the owner already writes.
|
||||
3. **Who creates the bucket.** The controller, from the catalogue, on every raise — a bucket exists
|
||||
from registration, like a seat's stream, so a reader can watch before the owner is assigned
|
||||
anywhere. Never a module.
|
||||
4. **The runtime's verbs.** `mesh/state.get`, `mesh/state.put`, `mesh/state.delete`,
|
||||
`mesh/state.keys`, `mesh/state.watch`, each naming the bucket as the module named it. A watch
|
||||
is answered once the current values are on their way, then each change is delivered to the
|
||||
bundle as a `mesh/state` request it answers — current values first (no deletions among them), an
|
||||
end-of-current marker, then changes. A child that restarts watches again, as it subscribes
|
||||
again. The runtime refuses, with the reason, a bucket the module was not issued, and a write to
|
||||
one it only reads.
|
||||
5. **Secrets.** None in a bucket, sealed or not: a bucket is a stream (design 32 §10). Sealed values
|
||||
are plain base64 and cannot be recognised, so the mechanical check is partial and said to be: the
|
||||
runtime refuses a value carrying a field whose name says it is a credential (`password`,
|
||||
`secret`, `token`, `authorization`, …), which catches the ordinary mistake and not a determined
|
||||
one. For the first consumer this has a concrete consequence: an MCP server registered with an
|
||||
authorisation header keeps that header out of the bucket.
|
||||
6. **History, lifetime, size.** One value per key unless the owner says more; no expiry unless it
|
||||
says one; a value at most 256 KiB and a bucket at most 64 MiB, the mesh's caps rather than a
|
||||
module's. **A bucket outlives its module's assignment** — what a module stored is data, and data
|
||||
outlives what declared it ([ADR 0030](../../02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md));
|
||||
unassigning is not cleaning up. A bucket whose declaration is gone is reported, never removed.
|
||||
7. **Events or state.** State (above).
|
||||
|
||||
## The work, once decided
|
||||
|
||||
1. A decision record, then design 32 (*state* becomes a relationship a module declares) and design
|
||||
25 (key-value buckets are part of the bus) amended.
|
||||
2. The controller: the manifest's two words and their registration check; buckets asserted on every
|
||||
raise; the grants for owners' and readers' runtimes; the buckets issued in each membership.
|
||||
3. The runtime: the five verbs, the watch delivery, the refusals; tested against a real server.
|
||||
4. The SDK, TypeScript and Go: a small state surface over the verbs.
|
||||
5. Proved on a running mesh with one small module, then handed to the operator's agent, whose
|
||||
registered servers move from events to a bucket.
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
status: graduated
|
||||
initiated: 2026-10-04
|
||||
touches:
|
||||
- 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.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/0126-a-module-declares-its-own-seats.md
|
||||
- 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md
|
||||
- 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
|
||||
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||
- 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||
- 03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md
|
||||
- 03-DESIGN/01-to-be/37-the-operators-machine.md
|
||||
- 03-DESIGN/01-to-be/38-building-the-operators-machine.md
|
||||
- 04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md
|
||||
became:
|
||||
- 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
|
||||
- 03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md
|
||||
---
|
||||
|
||||
# 025 — How a module plugs into the operator's shell
|
||||
|
||||
## What is investigated
|
||||
|
||||
The shell module writes the mesh's part of the account's shell startup file. But the shell is not the
|
||||
only module that needs a line there. A prompt theme loads itself from it. A language version manager
|
||||
sets a variable and sources its loader. A toolchain puts its directory on `PATH`. A desktop module
|
||||
names the browser. Today all of these sit in one hand-written file, and the shell module as written
|
||||
carries some of them in its own block and loads others only "if a module placed them". Nothing says
|
||||
how they get placed.
|
||||
|
||||
This effort asks four things:
|
||||
|
||||
1. **How a module contributes to the shell**: what it declares, who composes it, and in what order it
|
||||
lands.
|
||||
2. **Where the environment lives.** Variables and `PATH` entries are facts about the account, not
|
||||
lines of one shell's syntax. They should reach every shell (interactive or not), the login shell's
|
||||
`execute` verb, and programs a graphical session starts.
|
||||
3. **Where the operator's own lines go,** so that assigning the shell module loses nothing the
|
||||
machine does today.
|
||||
4. **Which part of a file the mesh owns.** ADR 0174 calls the kept region the operator's; the host and
|
||||
to-be 38 implement the inverse (the mesh owns a marked block, and everything outside it is the
|
||||
operator's). The record this becomes says which.
|
||||
|
||||
## Why
|
||||
|
||||
Rolling out the shell module (to-be 38 WP5) was stopped on 2026-10-04 after a review of what assigning
|
||||
it would do. Measured in [01](01-what-the-shell-file-holds-today.md):
|
||||
|
||||
- Every machine carries the same predecessor-written startup file, so the module's block would be
|
||||
appended after its own older copy and everything would run twice.
|
||||
- The block drops lines the machines rely on today.
|
||||
- Nothing installs the prompt theme or the plugins the block loads.
|
||||
- The `execute` verb runs a non-interactive login shell, which never reads the file the block is
|
||||
written into.
|
||||
|
||||
The operator's direction: other modules must be able to plug themselves into the shell; the prompt
|
||||
becomes its own module; assigning the shell module must lose no functionality; and the environment,
|
||||
`PATH` above all, needs an answer of its own.
|
||||
|
||||
## What it touches
|
||||
|
||||
- The manifest. A contribution to the shell is either a new use of the existing `contributes` /
|
||||
`receives` pair or a new gathered field like `jails` (to-be 31).
|
||||
- The controller's composition, if the controller assembles the text.
|
||||
- The `login-shell` seat (ADR 0176): what a holder must do with what is contributed to it, and
|
||||
whether a module or the mesh declares the seat. Possibly a new seat for the environment, beside it
|
||||
and beside the service manager's (ADR 0177). Research 023 asks the related question of a seat
|
||||
naming the files its holder owns.
|
||||
- ADR 0174's wording of the kept region, and ADR 0182's classification of the paths under a home.
|
||||
- The zsh module, and the modules this makes possible: an environment module, the prompt, a version
|
||||
manager, a toolchain.
|
||||
|
||||
## Where it stands
|
||||
|
||||
The operator proposed a separate **environment module**: one module, holding a mesh seat of its own,
|
||||
that alone writes the account's environment. It writes a file that shells source and the service
|
||||
manager's user environment, from the variables and `PATH` entries every other module contributes to
|
||||
it. That is the starting position for the environment ([02](02-how-a-module-plugs-in.md) §1, option
|
||||
E6). It leaves the shell's contribution as shell code only (§2), addressed to the `login-shell` seat,
|
||||
which moves into the mesh's own seat set beside the new `node-environment` (§6).
|
||||
|
||||
Graduated on 2026-10-04 with one change from the starting positions: the controller, not the
|
||||
environment module's own code, renders the environment into the module's files, so that the result
|
||||
is in the declaration before a machine applies it ([ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md),
|
||||
option 6b).
|
||||
|
||||
## Documents
|
||||
|
||||
- [01 — What the shell file holds today](01-what-the-shell-file-holds-today.md): evidence.
|
||||
- [02 — How a module plugs in](02-how-a-module-plugs-in.md): the options and the starting position.
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
# 01 — What the shell file holds today
|
||||
|
||||
Measured 2026-10-04 on the four machines of one installation: two servers and two workstations. All
|
||||
four have the account's login shell set to zsh, zsh installed from the distribution, and a
|
||||
predecessor-written startup file. The predecessor is retired, so nothing manages these files any more.
|
||||
|
||||
## The startup file is the same everywhere
|
||||
|
||||
The account's `~/.zshrc` is **byte-identical on all four machines**: 102 lines, one checksum.
|
||||
`~/.zshrc.local`, which the last line of `~/.zshrc` sources, comes in **two variants**: one shared by
|
||||
both servers, and one shared by both workstations. So the "per-machine" part is really a
|
||||
per-*kind*-of-machine part.
|
||||
|
||||
The predecessor produced these from one module with two *flavors*: a prompt flavor and an
|
||||
autocomplete flavor, each of which swapped in a different local file. Its install hook also:
|
||||
|
||||
- cloned the prompt theme and three plugins from their upstream repositories into `~/.zsh/`;
|
||||
- installed fonts;
|
||||
- changed the login shell.
|
||||
|
||||
On the workstations the theme and plugins are still on disk, left over and now owned by nothing. The
|
||||
servers have none of them.
|
||||
|
||||
## What the 102 lines are
|
||||
|
||||
Sorted by who should own each line once the machine is modules:
|
||||
|
||||
| Lines today | What they are | Natural owner |
|
||||
|---|---|---|
|
||||
| `EDITOR`, `VISUAL`, `XDG_CONFIG_HOME`, `PATH` gaining `~/.local/bin` and two script directories | the account's environment | the shell's default, or the environment itself |
|
||||
| `PATH` gaining a toolchain's directory | environment, for one tool | the toolchain's module |
|
||||
| a version manager's directory variable plus sourcing its loader | environment *and* shell code | the version manager's module |
|
||||
| two variables naming the operator's own script library | environment, the operator's own | the operator |
|
||||
| a variable that turns off an agent's terminal-title handling | environment, for one tool | the agent's module |
|
||||
| the terminal title hook, keybindings, `dircolors`, the `ls`/`grep` aliases, `ll`/`la`/`l`, a container-run alias, two disk-usage functions, two port aliases | interactive shell behaviour | the shell's default |
|
||||
| the prompt's instant-prompt cache, the theme, the prompt's own configuration file | shell code, order-sensitive (instant prompt first) | the prompt module |
|
||||
| autosuggestions, syntax highlighting (and, unloaded, an autocomplete plugin on disk) | shell code, order-sensitive (syntax highlighting last) | a plugin module, or the prompt module |
|
||||
| sourcing `~/.zshrc.local` | the operator's hook | the operator |
|
||||
|
||||
The workstation variant of the local file adds:
|
||||
|
||||
- more environment: a desktop toolkit theme, a file manager's plugin list, `BROWSER`, `VISUAL`
|
||||
overridden to a graphical editor, a language toolchain's binary directory on `PATH`;
|
||||
- two pieces of shell code: one that pads the prompt to the bottom of the terminal under a display, and
|
||||
one that sources a function file another module places;
|
||||
- a hook sourcing a further per-node file.
|
||||
|
||||
The server variant holds only that last module-placed source line.
|
||||
|
||||
**Count:** a workstation runs 65 non-comment lines from the two files (53 shared, 12 local); a server
|
||||
runs 54. Of a workstation's 65:
|
||||
|
||||
- about a quarter (15) are environment;
|
||||
- about half are interactive defaults no other module cares about;
|
||||
- the remaining quarter is other modules' code and hooks (a prompt, plugins, a version manager, an
|
||||
agent's functions), loaded from the shell file only because there was nowhere else to put it.
|
||||
|
||||
## The shell module as written
|
||||
|
||||
The `zsh` module of to-be 38 WP5 (catalogue change, unmerged):
|
||||
|
||||
- writes one block, appended at the end of `~/.zshrc`, holding a subset of the shared file:
|
||||
- its environment lines, minus the toolchain directory, the version manager and the agent variable;
|
||||
- the title hook, keybindings and the most common aliases, minus the port aliases;
|
||||
- guarded `source` lines for the theme and two plugins *if present*;
|
||||
- the source of `~/.zshrc.local`.
|
||||
- assigned to any of the four machines, appends that block after the identical lines already there, so
|
||||
every line in it runs twice, `~/.zshrc.local` included.
|
||||
- on the servers, the guarded prompt lines find nothing; nothing installs the theme anywhere.
|
||||
|
||||
## Which startup file reaches what
|
||||
|
||||
zsh's startup order, and what each path through it reads:
|
||||
|
||||
| started as | reads |
|
||||
|---|---|
|
||||
| interactive login (a console, ssh with a terminal) | `.zshenv`, `.zprofile`, `.zshrc`, `.zlogin` |
|
||||
| interactive non-login (a new terminal window) | `.zshenv`, `.zshrc` |
|
||||
| non-interactive login: `zsh -lc …`, what the `execute` verb runs | `.zshenv`, `.zprofile`, `.zlogin`, **not** `.zshrc` |
|
||||
| non-interactive: a script, `ssh host command` | `.zshenv` only |
|
||||
|
||||
So an environment written into `.zshrc` reaches neither `execute` nor a script. The distribution's
|
||||
system-wide login profile, which zsh's system `zprofile` sources, only ever *appends* to `PATH` when an
|
||||
entry is missing. An entry the account's `.zshenv` puts first therefore survives a login.
|
||||
|
||||
A graphical session's programs (a launcher, a bar, a window manager's key bindings) are started from the
|
||||
display manager and the service manager, not from a shell, and read none of these files. The service
|
||||
manager's own place for the account's environment is `~/.config/environment.d/`. Today it holds nothing
|
||||
on any of the four machines, so a program launched from the window manager does not see `PATH` entries
|
||||
that a terminal does.
|
||||
|
||||
## What the mesh already has for "many modules, one file"
|
||||
|
||||
Measured over the catalogue's 69 module definitions:
|
||||
|
||||
| mechanism | used by | shape |
|
||||
|---|---|---|
|
||||
| `contributes` / `receives` | 28 contribute, 15 receive | A consumer contributes **facts** keyed by a requirement. The provider receives all of them as one file in the mesh's own format, and **renders them itself**. "The controller does not know what a reverse proxy is." |
|
||||
| `listens` / `filtering` | 40 declare listens, 1 composes | The controller derives the whole firewall rule set from every module's ports and writes it where the holder asks. |
|
||||
| `jails` / `jailing` | 3 declare, 1 composes | Each module supplies its jail **in the tool's own format**. The controller assembles them, sorted, into the one file the holder names. |
|
||||
| `into: block` on a file | 2 | One module's marked region inside a file something else owns. Text outside the region is kept byte for byte. Placement is at the end, or at the start. |
|
||||
|
||||
None of these is a contribution of shell code or of environment today.
|
||||
@@ -0,0 +1,197 @@
|
||||
# 02 — How a module plugs in
|
||||
|
||||
Six questions, taken one at a time: the environment, shell code, the operator's own lines, order,
|
||||
who renders, and what a contribution is addressed to. Each has the options weighed and a starting
|
||||
position. The positions were set with the operator on 2026-10-04 and are what this effort tests, not
|
||||
what it has decided.
|
||||
|
||||
## 1. The environment: variables and `PATH`
|
||||
|
||||
A variable or a `PATH` entry is a fact about the account. It holds whichever shell is the login shell,
|
||||
and it is wanted by:
|
||||
|
||||
- every shell, interactive or not;
|
||||
- the login shell's `execute`;
|
||||
- a graphical session's programs.
|
||||
|
||||
[01](01-what-the-shell-file-holds-today.md) measures that `.zshrc` reaches only the first kind, and
|
||||
only interactively.
|
||||
|
||||
| | option | for | against |
|
||||
|---|---|---|---|
|
||||
| E1 | Each module writes lines into the shell's rc file (today) | nothing new | misses `execute`, scripts and the graphical session; written in one shell's syntax, so a second shell module starts over |
|
||||
| E2 | A module contributes environment facts (a variable and its value; a `PATH` entry and its position) **to the login shell**. The holder renders them into its shell's always-read file (`~/.zshenv` for zsh) | reaches every zsh, `execute` included; a contributor names no path and no shell | the graphical session sees none of it; the environment is tied to which module holds the shell; every shell module reimplements the same rendering |
|
||||
| E3 | E2, and the service-manager holder (ADR 0177) renders the same facts a second time into `~/.config/environment.d/` | the graphical session sees the same `PATH` as the terminal | one fact set, two owners, two renderings that can disagree; the service manager's module gains a duty unrelated to managing services |
|
||||
| E4 | One composed file in `environment.d` syntax, sourced by the shell with export-all | one file, two readers | ties the shell to the service manager's syntax, which is close to POSIX assignments but not equal (its `${VAR:-default}` and quoting rules differ); a value with a space breaks one reader or the other |
|
||||
| E5 | Shells take the environment from the service manager's environment generator, which prints the merged `environment.d` | no file of the shell's at all | every shell depends on the service manager and starts a process on every start; the generator's output is unquoted, so a value with a space breaks it |
|
||||
| **E6** | **An environment module.** A module of its own (working name `node-env`) holds a mesh seat, `node-environment`, and is the only writer of the account's environment. Every module contributes its variables and `PATH` entries to that seat. The holder writes them in each reader's format: a POSIX file of `export` lines that shells source, and the service manager's `~/.config/environment.d/` | the environment no longer depends on which shell holds `login-shell`; one owner and one rendering per format, both from the same facts; the graphical session included without the service manager's module; a contributor addresses "the environment", never a shell; the `PATH` rules (order, de-duplication) live in one module's code, where a test can hold them | one more module and seat, assigned on every node beside the shell; the `login-shell` protocol gains a duty, to source the environment file, which must be written down and checked |
|
||||
|
||||
**Starting position: E6.** It was the operator's proposal on 2026-10-04, and it replaces this
|
||||
document's first position (E2, then E3).
|
||||
|
||||
- The facts are the contribution. Each format is rendered once, by the one module whose subject is the
|
||||
environment.
|
||||
- A shell module's part shrinks to one line in its always-read file: `.zshenv` for zsh, sourcing the
|
||||
environment module's POSIX file. A bash or fish module writes the same line in its own file, and no
|
||||
contributor changes when the login shell does.
|
||||
|
||||
Sketched, for a node with zsh, the environment module, and a toolchain:
|
||||
|
||||
```
|
||||
toolchain ──contributes PATH entry──▶ node-environment ◀──contributes EDITOR, ~/.local/bin── zsh
|
||||
│ (held by node-env)
|
||||
┌──────────────────┴──────────────────┐
|
||||
▼ ▼
|
||||
POSIX export file ~/.config/environment.d/
|
||||
▲ ▲
|
||||
sourced from ~/.zshenv read by the service manager
|
||||
(every zsh, execute too) (the graphical session)
|
||||
```
|
||||
|
||||
The shell module still contributes its own environment (`EDITOR`, `XDG_CONFIG_HOME`, `~/.local/bin` on
|
||||
`PATH`) as a contributor like any other; it does not write those lines itself. Once issue 168 closes,
|
||||
the values a person varies become settings of whichever module contributes them (ADR 0174).
|
||||
|
||||
## 2. Shell code: a prompt, plugins, a version manager's loader
|
||||
|
||||
This *is* one shell's syntax, and order matters: a prompt's instant-prompt cache must run first, and
|
||||
syntax highlighting last.
|
||||
|
||||
| | option | for | against |
|
||||
|---|---|---|---|
|
||||
| S1 | **A contribution of code for one shell** (the shell it is for, the code, a slot), gathered by the controller and placed inside the holder's block in slot order. The same shape as `jails`, which a module supplies in fail2ban's own format and the controller assembles | a contributor names no path; the order is declared and checkable; unassigning the contributor removes its code at the next composition; a node holding fish simply has no zsh code rendered, and the resolver can say so | the controller gains one more gathered field; code for a shell travels in the declaration (in the clear, so no secrets in it, as for any file) |
|
||||
| S2 | **A drop-in directory**: each module places its own `~/.zsh/rc.d/NN-name.zsh`, and the shell's block sources the directory | no controller change; each file is its module's own, removed when undeclared | every contributor hard-codes a path inside the shell module's territory, against ADR 0112's spirit; order is a naming convention nothing checks; nothing ties the file to the shell actually being zsh |
|
||||
| S3 | Contributions as facts the holder renders (`contributes`/`receives` proper) | one mechanism with question 1 | code is not a fact; the holder would only paste it, which is S1 with an extra file |
|
||||
|
||||
**Starting position: S1.** A contribution to the shell carries **only code**, for named shells, each
|
||||
piece in a slot. Variables and `PATH` entries never go here; they go to the environment (§1). So a
|
||||
module touching both makes two contributions:
|
||||
|
||||
- A prompt module contributes zsh code in the first slot, and its own configuration file is its own
|
||||
owned file (ADR 0182).
|
||||
- A version manager contributes its directory variable to the environment, and its loader as code
|
||||
for each shell it supports.
|
||||
- A toolchain contributes a `PATH` entry to the environment and nothing to the shell.
|
||||
|
||||
What has to be settled: what each contribution is *addressed to*. Section 6 covers that.
|
||||
|
||||
## 3. The operator's own lines: the "local override"
|
||||
|
||||
Assigning the shell module must lose nothing the machine does today. That has two halves.
|
||||
|
||||
**What is common is the module's default, not an override.** The startup file is identical on all four
|
||||
machines ([01](01-what-the-shell-file-holds-today.md)). A line every machine has is the shell module's
|
||||
default, or another module's contribution. It is not a local override that a person would keep in step
|
||||
on every machine by hand. Most of today's file therefore moves into the shell module's block and into
|
||||
the contributions above. Little of it stays the operator's.
|
||||
|
||||
**What is the operator's is everything outside the mesh's block.** The host already works this way:
|
||||
|
||||
- the mesh's region is the marked block;
|
||||
- text outside it is kept byte for byte, and checked unchanged;
|
||||
- the region is given back when the module goes.
|
||||
|
||||
| | option | for | against |
|
||||
|---|---|---|---|
|
||||
| O1 | The mesh's block at the **start** of the file; the operator's lines after it | the operator's lines run last and win, which is what an override means; already supported (`at: start`) | a file the operator later rewrites must keep the markers; the host refuses a broken pair rather than guess |
|
||||
| O2 | A named operator region *inside* a file the mesh writes whole (ADR 0174's wording) | the file is entirely the mesh's except one hole | the opposite of what the host implements; a file a person already owns becomes the mesh's |
|
||||
| O3 | Only `~/.zshrc.local`, sourced from the block; `~/.zshrc` the mesh's whole | one obvious place | takes over a file the person owns today; ADR 0182 classifies the shell's own file as *written into*, not owned |
|
||||
|
||||
**Starting position: O1.** `~/.zshrc.local` keeps working because the operator's own lines source it,
|
||||
not because the mesh's block does.
|
||||
|
||||
The record this effort becomes corrects ADR 0174's description of the kept region as a **progressive
|
||||
insight**: the decision stands (a node varies a module by settings or by the operator's own lines,
|
||||
never by an edit), and only its description of which side is marked changes.
|
||||
|
||||
**The one-off migration** is a person's act, listed in the module's documentation (ADR 0182):
|
||||
|
||||
- remove from today's file every line the block or a contribution now carries;
|
||||
- keep the rest below the block.
|
||||
|
||||
Until a prompt module and the other contributors exist, the lines they will carry stay among the
|
||||
operator's own. Nothing is lost at any step.
|
||||
|
||||
## 4. Order
|
||||
|
||||
Order matters only for code. The environment is set before any code runs, because zsh reads
|
||||
`.zshenv` first. `PATH` entries carry their own position (before or after the system's), which the
|
||||
environment module orders, not the shell.
|
||||
|
||||
| | option | for | against |
|
||||
|---|---|---|---|
|
||||
| R1 | Numbers (`10`, `50`, `90`) | familiar | every contributor guesses a number; collisions are silent |
|
||||
| R2 | **A few named slots**, `first` / `normal` / `last`, with the module name breaking ties | the prompt says `first` and highlighting says `last` because that is what they mean; the composed result is the same bytes every time | three slots may not be enough |
|
||||
|
||||
**Starting position: R2.** Inside the shell module's block, the order is:
|
||||
|
||||
1. the line sourcing the environment module's file (in `.zshenv`, so it runs for every zsh; the rest
|
||||
of this list is `.zshrc`);
|
||||
2. the `first` slot;
|
||||
3. the shell module's own defaults;
|
||||
4. the `normal` slot;
|
||||
5. the `last` slot.
|
||||
|
||||
The operator's lines come after the block, as option O1 says.
|
||||
|
||||
## 5. Who renders: the controller or the holder's code
|
||||
|
||||
There are two different renderings, and E6 lets them be answered differently.
|
||||
|
||||
**The environment** is facts rendered into two fixed formats by the one module whose subject they are.
|
||||
|
||||
- The environment module receives the gathered contributions (the `contributes` / `receives` shape:
|
||||
facts in the mesh's own format, rendered by the receiver).
|
||||
- Its own code writes the POSIX file and the `environment.d` file whenever what it receives changes.
|
||||
That is ADR 0182's third class, written by the module's own process, owned by the account,
|
||||
atomically.
|
||||
- The controller learns no shell and no service manager. The `PATH` rules (prepend or append,
|
||||
de-duplicate, keep the system's entries) are ordinary code with ordinary tests.
|
||||
- To settle: what runs that code when the received file changes. The candidates are a host action
|
||||
that restarts on the received file, or a subscription through the runtime (ADR 0198).
|
||||
|
||||
**Shell code** is not facts. It is text in the shell's own syntax, assembled in slot order, which is
|
||||
what the controller already does for fail2ban jails: sort the pieces and concatenate them into the
|
||||
holder's region. The controller assembles; it never interprets the code. This keeps the shell module
|
||||
bundle-free for its files, and keeps the composed result visible in the declaration before a machine
|
||||
applies it.
|
||||
|
||||
## 6. What a contribution is addressed to
|
||||
|
||||
Under E6 there are two addressees: the environment and the login shell.
|
||||
|
||||
| | option | for | against |
|
||||
|---|---|---|---|
|
||||
| A1 | **Seats**: environment facts to `node-environment`, shell code to `login-shell`. Each seat's protocol says what its holder does with what is contributed to it | a contributor depends on a role ("the environment", "the login shell"), never on zsh or on one module; works the same for any holder | `login-shell` today is declared by the zsh module itself (ADR 0126). A second shell module may only claim it, never declare it, and the seat exists only while zsh's definition is registered |
|
||||
| A2 | Requirements the modules provide (`contributes` keyed by them, as the reverse proxy is) | an existing mechanism | a contributor on a node without the provider fails to resolve, though a toolchain's `PATH` entry with no environment module is merely unwritten |
|
||||
|
||||
**Starting position: A1, both seats in the mesh's own seat set** beside the service manager.
|
||||
|
||||
- `node-environment` is new, and is the mesh's from the start.
|
||||
- `login-shell` moves there from the zsh module's definition. A shell is as universal a role as a
|
||||
service manager, and a protocol that now carries duties (render the shell code contributed to it,
|
||||
source the environment file) should not depend on one module's registration.
|
||||
|
||||
Research 023 (a seat's protocol naming what its holder owns) is the general form of this: the
|
||||
environment seat would own the two environment files, and the login-shell seat the shell's
|
||||
startup-file region. The two efforts should not decide it twice.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Whether a contribution may be conditional on a capability: the workstation-only environment (a
|
||||
browser, a toolkit theme) is a desktop module's contribution, which arrives only where that module is
|
||||
assigned. Measured, this may need nothing new.
|
||||
- What a node without the environment module does with environment contributions: refuse them at
|
||||
resolve, or leave them unwritten and say so. The position here is to say so; a missing `PATH` entry
|
||||
is a visible gap, not a broken machine.
|
||||
- Whether the operator's own variables (the script-library paths in [01](01-what-the-shell-file-holds-today.md))
|
||||
are the operator's lines below the shell block, or a kept region of the environment module's file.
|
||||
The first needs nothing new, but reaches only interactive zsh.
|
||||
- How the prompt module and a plugin module divide the plugins. Packaging decides it as much as
|
||||
ownership: the plugins come from upstream repositories, not distribution packages, on these machines.
|
||||
- Whether the `execute` verb should read the interactive file at all once the environment is in
|
||||
`.zshenv`. The position here is no: a non-interactive login shell plus the environment is what a
|
||||
command needs, and the prompt's code should not run for it.
|
||||
- How a contribution reaches a second shell assigned beside the holder, which to-be 38 WP5 names as the
|
||||
first follow-up record. Under A1 a non-holder renders nothing, so the question becomes whether a
|
||||
non-holding shell module may render contributions for interactive use.
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
status: active
|
||||
initiated: 2026-10-04
|
||||
touches:
|
||||
- 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
|
||||
- 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
|
||||
- 03-DESIGN/01-to-be/37-the-operators-machine.md
|
||||
- 03-DESIGN/01-to-be/38-building-the-operators-machine.md
|
||||
- 04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md
|
||||
became: []
|
||||
---
|
||||
|
||||
# 026 — The graphical session as modules
|
||||
|
||||
## What is investigated
|
||||
|
||||
The workstations' graphical session as modules of the mesh, at the same level as the shell
|
||||
([to-be 41](../../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md)): a package, files
|
||||
under the account's home, a seat, and nothing that names a machine. The pieces are:
|
||||
|
||||
- the login manager;
|
||||
- how a session starts and what environment it gets;
|
||||
- the display server (X today, Wayland as a sibling);
|
||||
- the window manager (i3, and sway as its Wayland sibling);
|
||||
- the terminal emulator (xterm);
|
||||
- the session's companions: bar, compositor, launcher, notifier, lock and idle, clipboard,
|
||||
wallpaper, theming, fonts.
|
||||
|
||||
[To-be 38](../../03-DESIGN/01-to-be/38-building-the-operators-machine.md) names this WP7, and says
|
||||
each seat begins with a record naming its holders and verbs. [To-be 37](../../03-DESIGN/01-to-be/37-the-operators-machine.md)
|
||||
§4 leaves one question for the resolver: whether a held seat can gate another's assignment.
|
||||
|
||||
## Why
|
||||
|
||||
The operator asked for the graphical modules next, at the shell's level, and for one consistent
|
||||
experience across machines. Since the predecessor retired, nothing manages the workstations'
|
||||
desktops. Measured in [01](01-what-the-workstations-run.md):
|
||||
|
||||
- Two workstations carry one 983-line predecessor module's output, still byte-identical in its core.
|
||||
- One workstation also carries another machine's hardware fragments.
|
||||
- One runs a session that predates two fixes, with two notification daemons and two portals.
|
||||
- The session's environment is a hand-kept second copy of the account's, beside the one the mesh
|
||||
now writes.
|
||||
|
||||
## How it is approached
|
||||
|
||||
**Adopting is also improving** (the operator, 2026-10-04). A module is not a copy of what a machine
|
||||
does today. Making it is the moment to fix what is broken, drop what is dead, choose the better tool
|
||||
and remove the leftovers. Every module's design lists its improvements over today. **Every module
|
||||
also serves tools,** many of them, for reading, acting and diagnosing; a module that only places a
|
||||
package and a file is unfinished. The tools are catalogued in
|
||||
[026/05](../026-the-graphical-session-as-modules/05-the-tools-each-module-serves.md).
|
||||
|
||||
## What it touches
|
||||
|
||||
- **The seat table:** up to ten node seats.
|
||||
- **The resolver:** a seat held on a node gating another module's assignment.
|
||||
- **The contribution mechanism of ADR 0204:** whether it generalises beyond shells, or whether
|
||||
tools' own drop-in directories serve.
|
||||
- **The host's user-scoped units** (mesh-host #72, still open).
|
||||
- **Settings** for per-machine values (issue 168).
|
||||
- **ADR 0205's archive** for the two pieces the distribution does not package.
|
||||
|
||||
## Documents
|
||||
|
||||
- [01 — What the workstations run](01-what-the-workstations-run.md): evidence.
|
||||
- [02 — The questions and the options](02-the-questions-and-the-options.md)
|
||||
- [04 — Screensaver, displays and menus](04-screensaver-displays-and-menus.md): the lock and idle module, monitor layouts by the monitors' identity, rofi and dmenu behind one launcher seat, the clipboard, fonts
|
||||
- [05 — The tools each module serves](05-the-tools-each-module-serves.md): a first catalogue for the modules of 026 and 027
|
||||
- [03 — What the predecessor taught](03-what-the-predecessor-taught.md): its 128 modules and 3,395 commits, as patterns to keep and failures not to repeat; shared with research 027.
|
||||
@@ -0,0 +1,141 @@
|
||||
# 01 — What the workstations run
|
||||
|
||||
Measured 2026-10-04 on the two workstations of one installation, read-only: a laptop with a hybrid
|
||||
GPU and an internal panel, and a desktop with one GPU and two external monitors. Both run the same
|
||||
predecessor-generated desktop. File equality was checked by checksum across the two machines.
|
||||
|
||||
## How a session starts
|
||||
|
||||
The chain is the same on both:
|
||||
|
||||
1. The login manager (`lemurs`, built from the distribution's user repository, its package now in
|
||||
the official one) runs its X setup script on a virtual terminal.
|
||||
2. That script sources the login shell's profile files, then `~/.xprofile`, then the system's
|
||||
`xinitrc.d` drop-ins, then merges `~/.Xresources`.
|
||||
3. `~/.xprofile` reuses the systemd user manager's bus, then sources `~/.xinitrc`.
|
||||
4. `~/.xinitrc` sets up the session and ends with `exec i3`.
|
||||
|
||||
The login manager's own window-manager entry (`exec startx`) is never reached. Its configuration
|
||||
file uses a format two releases old, and an unmerged newer one sits beside it.
|
||||
|
||||
**What `~/.xinitrc` does**, in order:
|
||||
|
||||
1. Sources the system drop-ins, which import `DISPLAY` and `XAUTHORITY` into the user manager.
|
||||
2. Starts the keyring and exports its ssh socket.
|
||||
3. Exports the session's environment:
|
||||
- `PATH`, with nine entries, one of them a directory that no longer exists;
|
||||
- toolchain variables;
|
||||
- `XDG_CONFIG_HOME` and `XDG_DATA_DIRS` (with flatpak);
|
||||
- five GTK/Qt theme variables;
|
||||
- the desktop's identity (`XDG_CURRENT_DESKTOP`, `XDG_SESSION_DESKTOP`);
|
||||
- three of the operator's own variables.
|
||||
4. Imports an explicit allowlist of ten of those into the user manager and D-Bus activation. It is
|
||||
never `--all`, because:
|
||||
5. a predecessor file of **secrets as environment variables** (package-registry and API tokens) is
|
||||
sourced next.
|
||||
6. Sets the screensaver and display power timeouts, restores the wallpaper, and starts the lock
|
||||
watcher in a respawn loop. It is deliberately not a unit, because it needs the login session.
|
||||
7. `exec i3`.
|
||||
|
||||
**The account's environment, as of today, has three sources that disagree:**
|
||||
|
||||
- this file, for the session;
|
||||
- the mesh's `environment.sh`, for shells
|
||||
([ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md));
|
||||
- `~/.config/environment.d/`, for the user manager. It holds the mesh's `50-mesh.conf`, and a
|
||||
predecessor file that **sets `PATH` outright** and sorts after it.
|
||||
|
||||
## The roles, and what fills them
|
||||
|
||||
| role | software | where configured |
|
||||
|---|---|---|
|
||||
| login manager | lemurs | `/etc/lemurs/*` (identical on both, and to the predecessor's source) |
|
||||
| session start and environment | the login manager's X setup, `~/.xprofile`, `~/.xinitrc`, `xinitrc.d`, the D-Bus import, `environment.d` | `~/.xprofile`, `~/.xinitrc`, `~/.config/environment.d/*` |
|
||||
| display server | Xorg (`xorg-server`, `xinit`, the X apps; vendor drivers per GPU) | **no** `xorg.conf.d`; monitors by `xrandr` scripts |
|
||||
| monitor layout | `xrandr` scripts (arandr), a hotplug rule on the laptop | `~/.screenlayout/`, a scripts folder, a window-manager fragment |
|
||||
| window manager | i3 4.25 | `~/.config/i3/config` and `config.d/*`, a reload watcher (user unit) |
|
||||
| bar | i3bar with i3status-rust | `~/.config/i3status-rust/*`, 14 themes, a bar watchdog (user unit) |
|
||||
| terminal | xterm (the only terminal installed) | `~/.Xresources.d/xterm`, the window manager's binding, the compositor's opacity rule |
|
||||
| compositor | picom | `~/.config/picom/picom.conf` |
|
||||
| launcher and menus | rofi | `~/.config/rofi/*`, launcher, power-menu and theme-picker scripts |
|
||||
| notifier | dunst (D-Bus activated) | `~/.config/dunst/dunstrc`, `dunstrc.d/*` |
|
||||
| lock, idle, display power | xss-lock and i3lock-color, `xset` | `~/.xinitrc`, a lock script |
|
||||
| clipboard | greenclip, xclip | `greenclip.toml` |
|
||||
| wallpaper | feh | `~/.fehbg` (points into the predecessor's tree) |
|
||||
| theming | Adwaita dark, qt5ct/qt6ct, the desktop portal (GTK backend pinned) | GTK `settings.ini`, `qt*ct.conf`, `portals.conf`, an appearance script, `.Xresources` cursor |
|
||||
| fonts | Hack and Meslo Nerd fonts in `~/.local/share/fonts` (not packaged), noto | `~/.Xresources.d/xft` (DPI fixed at 96) |
|
||||
| keyboard | nothing set; the default layout; vendor keys via triggerhappy on the laptop | window-manager bindings, `/etc/triggerhappy` |
|
||||
|
||||
**Packages:** every piece except two is in the distribution's official repositories, and the login
|
||||
manager now is too. The two exceptions are the lock screen's colour build (`i3lock-color`) and the
|
||||
clipboard manager (`rofi-greenclip`). The Nerd fonts exist as official packages, but both machines
|
||||
carry hand-copied files instead.
|
||||
|
||||
## Identical, different, and why
|
||||
|
||||
**Byte-identical on both machines:**
|
||||
|
||||
- the session files: `.xinitrc`, `.xprofile`, `.Xresources` and its drop-ins;
|
||||
- the i3 main configuration and two of its fragments;
|
||||
- the bar's top configuration and themes;
|
||||
- picom, rofi, the GTK and Qt settings, the portal configuration, the login manager.
|
||||
|
||||
**Different, by cause:**
|
||||
|
||||
| cause | what |
|
||||
|---|---|
|
||||
| hardware | the monitor layout script; the bar's battery block; the laptop's power and vendor-key units and udev rules |
|
||||
| misassignment | the desktop carries the **laptop's** hardware fragments: the vendor-key daemon and its triggers, the backlight rule, the brightness drop-in, a touchpad reset, and the laptop's monitor layouts, in an older version |
|
||||
| drift | the notifier's position and corner radius; a "temporary" window-manager fragment from a test; the bar watchdog disabled; a second Qt configuration tool; different font builds |
|
||||
| a stale session | the desktop's session began before two fixes, so it runs two notification daemons and two portals, and its user manager lacks the desktop's identity |
|
||||
|
||||
**Dead references:** the window manager starts a polkit agent that is installed on neither machine,
|
||||
so there is no polkit agent at all. `PATH` names a directory that does not exist.
|
||||
|
||||
**Per-machine values inside shared files:**
|
||||
|
||||
- the DPI;
|
||||
- absolute home paths, in the clipboard configuration and the flatpak data directories;
|
||||
- the laptop's panel name, inside a fragment both machines carry.
|
||||
|
||||
## User units the desktop needs
|
||||
|
||||
| unit | does | laptop | desktop |
|
||||
|---|---|---|---|
|
||||
| reload watcher | reloads the window manager and bar when their files change | on | on |
|
||||
| bar watchdog | restarts a dead bar | on | off |
|
||||
| clipboard daemon | from its package | via the window manager | unit **and** window manager |
|
||||
| vendor power profile, memory guard | laptop power | on | — |
|
||||
|
||||
None is managed. Applying them as the account needs the host's user scope
|
||||
([ADR 0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)),
|
||||
which is still an open change.
|
||||
|
||||
## The predecessor's module
|
||||
|
||||
One manifest of 983 lines covers the window manager, bar, launcher, notifier, compositor, lock
|
||||
screen, session bootstrap, theming and scripts. It:
|
||||
|
||||
- has four *flavors*: i3, laptop (i3 plus the monitor wizard and hotplug), desktop (i3 plus
|
||||
nothing) and a laptop model (laptop plus vendor keys);
|
||||
- has about **105 theme variables** substituted into templates: border, gaps, fonts, workspace
|
||||
names, every colour of bar, launcher, notifier and lock screen, compositor opacity, cursor, idle
|
||||
times, Qt and GTK theme names;
|
||||
- enables the two user units from an install hook.
|
||||
|
||||
Separate modules held the login manager and the display server (one flavor, `xorg`, with a comment
|
||||
calling `wayland` "the intended sibling"). The shell module held no graphical part.
|
||||
|
||||
## Wayland and sway
|
||||
|
||||
**Nothing exists.** There is no compositor, no sway configuration, no Wayland session entry, and the
|
||||
login manager's Wayland directory is empty. What is installed is libraries:
|
||||
|
||||
- Wayland itself and the Qt Wayland plugins, which other packages pull in;
|
||||
- `xwayland`, explicitly installed and required by nothing;
|
||||
- on the desktop, an orphaned compositor library from another desktop environment, and that
|
||||
environment's portal backend, pulled in by a game launcher. The portal configuration pins
|
||||
against it.
|
||||
|
||||
Every piece a sway session needs is in the official repositories: the compositor, its lock screen,
|
||||
a terminal (`foot`), a bar (`waybar`), a notifier (`mako`) and `xwayland`.
|
||||
@@ -0,0 +1,123 @@
|
||||
# 02 — The questions and the options
|
||||
|
||||
Seven questions. Each has its options and a starting position, which is what this effort tests, not
|
||||
what it has decided.
|
||||
|
||||
## 1. How finely the desktop splits into modules
|
||||
|
||||
| | option | for | against |
|
||||
|---|---|---|---|
|
||||
| G1 | One desktop module, as the predecessor had | one assignment | flavors again, per machine; ADR 0174 refuses them, and the evidence shows a flavor landing on the wrong machine |
|
||||
| G2 | **One module per piece of software:** `lemurs`, `xorg`, `i3`, `i3status-rust`, `xterm`, `picom`, `rofi`, `dunst`, `xss-lock` with the lock screen, `greenclip`, `feh`, a theme module, a fonts module | each is what it declares; a machine gets exactly what is assigned; the same split already works for the shell and its plugins | about thirteen assignments per workstation |
|
||||
| G3 | G2, plus a named **set** the controller assigns as one (for example *the X desktop*) | G2's precision with G1's convenience | a set is a new controller concept |
|
||||
|
||||
**Starting position: G2.** Whether a set is worth a record is left until the thirteen assignments
|
||||
have been done by hand once.
|
||||
|
||||
## 2. The seats
|
||||
|
||||
Research 018 listed the candidates. ADR 0204 has since put the login shell in the mesh's own set,
|
||||
because a role with a protocol should not depend on one module's registration. The same reasoning
|
||||
applies here:
|
||||
|
||||
| seat | holders | protocol, first verbs |
|
||||
|---|---|---|
|
||||
| `node-login-manager` | lemurs, greetd | which sessions it offers, the default session |
|
||||
| `node-display-server` | xorg, sway | `displays`, `layout` |
|
||||
| `node-display-session` | i3, sway | `reload`, `workspaces`, `windows` |
|
||||
| `node-terminal-emulator` | xterm, foot, alacritty | which terminal `$TERMINAL` names; `open` |
|
||||
| `node-bar`, `node-compositor`, `node-launcher`, `node-notifier`, `node-lock-screen`, `node-clipboard` | the pieces above, and their Wayland counterparts | one verb or none each, until a use asks for one |
|
||||
|
||||
**A compositor that is its own server holds two seats.** Sway is both the display server and the
|
||||
display session. A module may claim several seats, so this needs nothing new.
|
||||
|
||||
**Starting position:** the first four seats are in the mesh's own set. The companion seats are
|
||||
added only as each holder is written; for those, a module without a seat is acceptable at first.
|
||||
|
||||
## 3. One module requiring another seat to be held
|
||||
|
||||
i3 needs an X server held on its node, and sway needs nothing below it. A terminal needs a session.
|
||||
To-be 37 left open how that is said.
|
||||
|
||||
| | option | for | against |
|
||||
|---|---|---|---|
|
||||
| R1 | A seat **delivers a provision** (`x11-display`, `wayland-display`) and a module requires it at node scope. The seat table has a `delivers` field already, and requirements already resolve | existing machinery; the refusal names the seat and its possible holders, which design 27 already lists | a node-scoped requirement that never crosses machines has to be stated as such |
|
||||
| R2 | A new field, *needs the seat X held* | reads plainly | a second way to say what R1 says |
|
||||
| R3 | Nothing; assign carefully | — | the mistake the evidence shows (a laptop's fragments on a desktop) is exactly an unchecked assignment |
|
||||
|
||||
**Starting position: R1.** `xorg` and `sway` each deliver what they serve. `i3`, `picom` and `xss-lock`
|
||||
require `x11-display`. `foot` requires a Wayland display, and xterm requires an X one, which a Wayland
|
||||
session gives through `xwayland`.
|
||||
|
||||
## 4. Who starts the session, and with what environment
|
||||
|
||||
Today `~/.xinitrc` is a hand-kept second environment and the session's whole start script.
|
||||
|
||||
| | option | for | against |
|
||||
|---|---|---|---|
|
||||
| S1 | The display server's module writes `~/.xinitrc` **into**: a mesh block at the start that sources the account's environment (`environment.sh`), merges the X resources, and runs the session's contributed start lines. The session holder's module contributes its `exec` line. The operator's lines stay after the block | one environment for shells, the session and the user manager; nothing to keep in step | the order inside `.xinitrc` becomes the slot order of a contribution (question 5) |
|
||||
| S2 | The login manager's module owns the session script under `/etc` | system scope; no home file | the environment is the account's, and the script is the same for every account |
|
||||
| S3 | Leave `.xinitrc` the operator's | nothing to build | the third environment stays |
|
||||
|
||||
**Starting position: S1.**
|
||||
|
||||
- The desktop's identity (`XDG_CURRENT_DESKTOP`) and the theme variables become **environment
|
||||
contributions** (ADR 0203) from `i3` and from the theme module. They then also reach the user
|
||||
manager through `environment.d`, which replaces most of today's allowlist import.
|
||||
- The secrets file stays out of the environment until research 027 settles how a secret reaches an
|
||||
account.
|
||||
|
||||
## 5. How other modules contribute to a holder's file
|
||||
|
||||
The terminal's settings are X resources. A bar, a launcher binding and a hardware module's key
|
||||
bindings are window-manager configuration. Autostarts are the session's. ADR 0204 built slot
|
||||
contributions for shells only.
|
||||
|
||||
| | option | for | against |
|
||||
|---|---|---|---|
|
||||
| C1 | **The tool's own drop-in directory**, where it has one: i3's `include`, dunst's `dunstrc.d`, X resources' `#include`, XDG autostart entries, `environment.d`. Each contributor owns its own file there | no mesh change; the tools already read these directories; unassigning removes the file | each contributor names a path in another tool's directory (ADR 0204 rejected this for shells, where no drop-in convention exists); ordering is by file name |
|
||||
| C2 | **ADR 0204's mechanism generalised:** `contributes` text *for a format* (`zsh`, `xresources`, `i3`, `xinitrc`) in a slot, placed by the holder's placeholder | one mechanism, checked by the controller, order declared | every format must be named in the controller; a bigger change to ADR 0204 |
|
||||
| C3 | C1 where the tool has a drop-in convention, C2 where it does not (`.xinitrc`, `.Xresources` order) | uses each tool's own grain | two mechanisms to learn |
|
||||
|
||||
**Starting position: C3**, with the boundary drawn by the tools. A tool that reads a directory gets
|
||||
drop-ins. A file without one gets slots. This means amending ADR 0204's "shell" to "a format", which
|
||||
is a progressive extension rather than a reversal.
|
||||
|
||||
## 6. What varies per machine
|
||||
|
||||
| what | today | option |
|
||||
|---|---|---|
|
||||
| monitor layout | per-machine `xrandr` scripts, monitor names baked in | a **setting** of `xorg` (issue 168), and a `layout` verb of the display server seat |
|
||||
| DPI, fonts' size | fixed in an X resource | a setting |
|
||||
| battery block, vendor keys, brightness, touchpad | a laptop model's flavor | **a hardware module** per machine model, contributing its window-manager fragment, bar block and udev rules. The desktop simply is not assigned it |
|
||||
| theme (the 105 variables) | template substitution | settings of each tool's module, after issue 168 closes (ADR 0174). Until then each module carries today's values as its default |
|
||||
|
||||
**Starting position:**
|
||||
|
||||
- Hardware modules for what follows the machine.
|
||||
- Defaults now, settings after issue 168, for what the operator varies.
|
||||
- The monitor layout waits for settings. Until then it is an operator-owned script the display
|
||||
server's block calls if present.
|
||||
|
||||
## 7. Wayland and sway
|
||||
|
||||
Nothing of a Wayland session exists, and every piece is officially packaged. "Wayland" is a protocol,
|
||||
not a piece of software, so it has no module of its own. Its parts are `sway` (server and session),
|
||||
`swaylock`, `foot`, `waybar`, `mako`, and `xwayland` for X clients.
|
||||
|
||||
**Starting position:**
|
||||
|
||||
- The seats and the requirements (questions 2 and 3) are designed so that sway fits from the first
|
||||
day.
|
||||
- The X stack is built first, because it is what runs.
|
||||
- `sway` and its companions are written after that, and proven on one workstation as a second
|
||||
session the login manager offers beside i3. That lets the operator try it without losing the
|
||||
working desktop.
|
||||
|
||||
## Prerequisites this effort cannot remove
|
||||
|
||||
- **User-scoped units** (mesh-host #72) for the reload watcher and the bar watchdog.
|
||||
- **Settings** (issue 168) for monitors and theme values.
|
||||
- **The two packages not in the official repositories:** the lock screen's colour build and the
|
||||
clipboard manager. Each is ADR 0205's case, a pinned archive, or a choice of an official
|
||||
alternative (`i3lock` without colours; `clipmenu`/`cliphist`).
|
||||
@@ -0,0 +1,51 @@
|
||||
# 03 — What the predecessor taught
|
||||
|
||||
A study on 2026-10-04 of the retired predecessor:
|
||||
|
||||
- its 128 module manifests, their hooks, its installer and its sync engine;
|
||||
- 3,395 commits of history;
|
||||
- what it left on four machines.
|
||||
|
||||
This document holds what bears on the graphical session and on the system layer
|
||||
([research 027](../027-the-system-layer-as-modules/00-overview.md)). The evidence is in the
|
||||
predecessor's history. A commit is cited here by what it fixed, not by its hash, because the
|
||||
repository is private.
|
||||
|
||||
## Keep: what worked
|
||||
|
||||
| pattern | where it shows | in the mesh |
|
||||
|---|---|---|
|
||||
| ownership marked inside the file: inside the markers is reconciled, outside is kept verbatim | a block marker in a shared file, after an engine that rewrote whole files | kept regions (ADR 0174, ADR 0204) |
|
||||
| two writers get two files and an `include`, the include first | the ssh client's configuration, after two writers fought over one file | ADR 0203's two files; research 026 C1 |
|
||||
| one writer per file, one authority per action | only the reload watcher restarts the window manager, after three mechanisms each did | ADR 0182 |
|
||||
| refuse to write when the source of truth is unreadable; never empty a block because a query found nothing | a block of names was emptied by a failed query | — keep |
|
||||
| an unresolved template variable fails the install | a literal unfilled path was installed green | [issue 231](../../04-ISSUES/231-a-misspelled-placeholder-is-written-out-as-text/00-report.md): the mesh still has this gap |
|
||||
| prune only what you can prove you placed | stale files from earlier deliveries | ADR 0189 |
|
||||
| ensuring never rotates a credential | a silent rotation caused a retry storm, a ban of the shared address and a lost registry | ADR 0114 |
|
||||
| vendor only the files you use; never clone and link | three files instead of 77 MB | ADR 0205 |
|
||||
| copy, never symlink | a recursive delete followed a link, and every reinstall failed | ADR 0012 |
|
||||
| verification says what it did not check | a verifier said *clean* while the secret was still on disk | — keep |
|
||||
| alert once per condition | 411 alerts hid a 28-hour outage | ADR 0090 |
|
||||
|
||||
## Do not repeat
|
||||
|
||||
| failure | what it did | the mesh instead | where the mesh is still exposed |
|
||||
|---|---|---|---|
|
||||
| **Flavors** | variant files and packages per machine type: a gate dropped, the first-seen variant won, packages never installed, the verifier ignored the gate. On the day of the study a desktop carried a laptop model's fragments | one module per piece, assignment per machine (ADR 0174, research 026 §1) | a setting that switches which whole file is rendered is a flavor under another name |
|
||||
| **The freeze** | existing values outranked new defaults; templated files were rendered once (*copy if absent*) | files are generated (ADR 0011) | a created-once file (ADR 0087) is a deliberate freeze, and a push must say *kept* |
|
||||
| **Adopting drift** | a *merge* strategy made a local edit the record forever; switching strategies clobbered a person's model choice | nothing is read back (ADR 0174) | an edit outside a kept region is overwritten **silently**. The predecessor's *why is this back* loop: the push should name what it overwrote |
|
||||
| **Environment templating** | `${VAR}` matched any name; unresolved names stayed literal; comments and destination paths were interpolated | namespaced placeholders; `$` forbidden in contributed values (ADR 0203) | issue 231 |
|
||||
| **Hooks with privilege** | install hooks ran `sudo`, `chsh`, `systemctl`, `git clone` and `curl`, and swallowed failures into a warning | the `user` shape, the service shape, archives (ADR 0176, 0177, 0205) | the agent module writes under `/etc` from its own tool through `sudo` (no keep-original, no give-back); the prompt's helper downloads itself unpinned; that the operator escalates without a prompt is assumed by three modules and declared by none (research 027) |
|
||||
| **Secrets in environment files** | `.env` files left world-readable; the decryption key beside what it decrypts; a deleted secret stayed in the file, so rotation was a no-op | the vault (ADR 0113, 0114) | a predecessor file of secrets is still sourced into the graphical session on two machines (research 027 Q2) |
|
||||
| **Symlinks into a home** | a system file linked into a person's home | ADR 0012 | on the control machine, a fail2ban action file is still a predecessor link into its home tree. Deleting that tree would silently break the repeat-offender jail. The mesh's fail2ban module must own it as a file first |
|
||||
| **Green while broken** | a recorded version frozen for four months; a verifier passing what it skipped | ADR 0134, 0145, 0184 | issue 230: a plan waiting for ever reads as healthy |
|
||||
|
||||
## What it means here
|
||||
|
||||
- **Research 026:** the desktop's 88 flavor-gated files and 92 theme variables are the flavor and
|
||||
templating failures in one module. Question 1 (one module per piece) and question 6 (hardware
|
||||
modules, settings later) are the answer, and nothing in the new modules may switch whole files on a
|
||||
setting.
|
||||
- **Research 027:** the hooks that installed the AUR helper, enabled the login manager and changed
|
||||
shells are what the `package`, `service` and `user` shapes replace. Every remaining `sudo` in a module's
|
||||
own code is a debt to be named, starting with the agent module.
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
# 04 — Screensaver, displays and menus
|
||||
|
||||
Three areas the operator named on 2026-10-04, as their own modules. Each sharpens a row of
|
||||
[01](01-what-the-workstations-run.md) and a question of [02](02-the-questions-and-the-options.md).
|
||||
|
||||
## The screensaver: idle, lock and display power
|
||||
|
||||
**Measured on both workstations:**
|
||||
|
||||
- **Idle and lock** are three things wired by hand in the session's start script:
|
||||
- the X screensaver timeout (`xset s 1800`);
|
||||
- the display power timeouts (`xset dpms`);
|
||||
- `xss-lock` running the colour build of `i3lock` through a wrapper, in a respawn loop.
|
||||
- **A second screensaver,** xscreensaver, is installed and deliberately not started. Earlier it
|
||||
overrode the display power settings with its own, and locked nothing. Its configuration file is
|
||||
still in the home.
|
||||
- **The lock screen's 20-odd colours and formats** were predecessor theme variables.
|
||||
- **The colour build is not in the official repositories** (research 026/01).
|
||||
|
||||
**Starting position:**
|
||||
|
||||
- **One module for the lock screen,** holding `node-lock-screen`: the locker and its wrapper as the
|
||||
module's own files, the screensaver and display power timeouts, and `xss-lock`.
|
||||
- The timeouts and colours are its defaults, and settings later (issue 168).
|
||||
- The colour build ships as ADR 0205's pinned archive, or the module uses the official `i3lock`.
|
||||
That is the operator's choice, and the colours are the only difference.
|
||||
- xscreensaver is not a module; its package and file are removed.
|
||||
- `xss-lock` needs the logind session, so it stays a session-start line contributed into
|
||||
`.xinitrc`'s block (question 4), not a unit.
|
||||
|
||||
## Monitor layout (xrandr)
|
||||
|
||||
**Measured:**
|
||||
|
||||
- Each workstation has a layout script generated by `arandr`, with the monitor names baked in. One
|
||||
workstation also has several layouts for named places, a hotplug rule and a wizard.
|
||||
- **The desktop carried the laptop's layout scripts.**
|
||||
- No `xorg.conf.d`, and no layout tool beyond the scripts.
|
||||
|
||||
**Starting position: `autorandr`** (official repositories) inside the display server's module.
|
||||
|
||||
- `autorandr` saves a layout as a profile **keyed by the connected monitors' identities** (their EDID)
|
||||
and applies the matching one at login and on hotplug.
|
||||
- Profiles therefore need no machine's name. A profile can be shared mesh-wide and simply never
|
||||
matches on a machine without those monitors. That is exactly the "say it by what is there, never by
|
||||
a name" rule (ADR 0112).
|
||||
- The profiles are the operator's data, saved by the tool itself, so they are *found* (ADR 0182). A
|
||||
`layout` verb on `node-display-server` lists, saves and applies them.
|
||||
- The arandr scripts and the hotplug rule retire once a profile exists for each.
|
||||
|
||||
## Menus: rofi and dmenu
|
||||
|
||||
**Measured:**
|
||||
|
||||
- rofi is the launcher, the power menu, the theme picker and the clipboard menu.
|
||||
- The operator's scripts call `rofi -dmenu` in four places and **plain `dmenu` in two. dmenu is
|
||||
installed on neither workstation, so those two fail.**
|
||||
|
||||
**Starting position:**
|
||||
|
||||
- **`rofi` holds `node-launcher`**, and the seat's protocol includes a **dmenu-compatible command**:
|
||||
read choices on standard input, print the chosen one. Scripts call that command, not a program by
|
||||
name.
|
||||
- **`dmenu` is a module of its own** (official repositories), able to hold the same seat on a machine
|
||||
that wants it, for instance a Wayland session where `wofi` or `fuzzel` would hold it instead.
|
||||
- The rofi module carries its theme files, and the menus that belong to other modules arrive as those
|
||||
modules' scripts:
|
||||
- power menu → the session;
|
||||
- clipboard menu → the clipboard module;
|
||||
- theme picker → settings, once issue 168 closes.
|
||||
|
||||
## The clipboard: xclip and greenclip
|
||||
|
||||
**Measured:**
|
||||
|
||||
- **greenclip** keeps the clipboard's history, and rofi shows it on a key binding.
|
||||
- **greenclip is not in the official repositories.**
|
||||
- It is started two ways: the window manager's configuration starts it on both workstations, and on
|
||||
one a user unit is enabled as well.
|
||||
- Its configuration names an absolute home path.
|
||||
- **xclip** (official) is the command-line clipboard the operator's scripts use.
|
||||
|
||||
**Starting position:**
|
||||
|
||||
- **`xclip` is a module of its own,** a package and nothing else. It is the tool scripts depend on,
|
||||
and a module that needs it requires it.
|
||||
- **The clipboard manager holds `node-clipboard`:** its daemon, started once by the session (a session
|
||||
contribution, or a user unit once user-scoped units ship, never both), its configuration with no
|
||||
absolute path, and its menu binding contributed to the window manager.
|
||||
- **Which manager holds it is the operator's choice:**
|
||||
- greenclip, as today, shipped under ADR 0205;
|
||||
- or `clipmenu` (official), which feeds the same dmenu-compatible command as the launcher seat above,
|
||||
and needs no archive.
|
||||
- **On Wayland** the same seat is held by `cliphist` with `wl-clipboard`, both official.
|
||||
|
||||
## Fonts
|
||||
|
||||
**Measured:**
|
||||
|
||||
- The fonts the desktop uses are **hand-copied files** in the account's font directory, not packages:
|
||||
- a Nerd font for the window manager, the bar and the terminal;
|
||||
- a second one for the prompt;
|
||||
- on one workstation, the same four files twice, once under URL-encoded names;
|
||||
- on the other, a different build of the same font and three more copied from a theme's repository.
|
||||
- The system's default monospace is a different font (`Noto Sans Mono`), so anything that asks for
|
||||
`monospace` gets another face than the terminal.
|
||||
- The DPI is fixed in an X resource.
|
||||
- **Every Nerd font in use is in the official repositories** (Hack, Meslo, Iosevka, JetBrains Mono).
|
||||
|
||||
**Decided** (the operator left the choice open, except that it must not be today's Hack):
|
||||
|
||||
| role | face | why |
|
||||
|---|---|---|
|
||||
| monospace: terminal, window manager, bar, launcher, prompt | **JetBrains Mono Nerd Font** | built for long reading in a terminal, unambiguous `0O1lI`, optional ligatures; a version-3 Nerd font, so every icon the prompt and bar use is present |
|
||||
| interface: GTK, Qt, notifications | **Inter** | designed for screens, clear at small sizes |
|
||||
| icons missing from any face | Nerd Fonts Symbols | a fallback, so a font without icons still shows them |
|
||||
| emoji | Noto Color Emoji | |
|
||||
| serif and every other script | Noto | |
|
||||
|
||||
All five are official packages.
|
||||
|
||||
**Starting position:**
|
||||
|
||||
- **A `fonts` module:** those packages, and a fontconfig file it owns that maps `monospace`, `sans-serif`,
|
||||
`serif` and the emoji and symbol fallbacks to the chosen faces, so every program agrees.
|
||||
- The terminal, bar, launcher and prompt modules name the family, not a file.
|
||||
- The DPI becomes the display server's setting (issue 168).
|
||||
- The copied files are removed by the operator once the packages are in (ADR 0182).
|
||||
- Fonts are not a seat: several coexist. The module owns the one place where *the* default is said.
|
||||
@@ -0,0 +1,77 @@
|
||||
# 05 — The tools each module serves
|
||||
|
||||
A first catalogue for the modules of research 026 and 027, as the operator asked: "all kinds of useful
|
||||
tools for all these modules". Each tool is served by the node's runtime (ADR 0175), on the machine the
|
||||
module runs on. Through discovery (ADR 0195) it is reachable from any machine as
|
||||
`<machine>/<module>.<tool>`, or as `<machine>/<seat>.<verb>` where a seat defines it.
|
||||
|
||||
**Conventions:**
|
||||
|
||||
- **(r)** reads.
|
||||
- **(a)** acts on the machine, escalating where it must, as the packet filter does (to-be 38 WP4).
|
||||
- **(d)** is a desktop act that needs the operator's session.
|
||||
- A tool that changes something a module declares says so in its answer: the next push restores the
|
||||
declaration.
|
||||
- Every tool answers structured data, not prose (issue 229).
|
||||
- **Seat verbs** (marked *seat*) are the protocol every holder of that seat serves. The rest are the
|
||||
module's own.
|
||||
|
||||
## The graphical session (026)
|
||||
|
||||
| module | tools |
|
||||
|---|---|
|
||||
| `xorg` (*node-display-server*) | *seat* `displays` (r: outputs, modes, rates, connected monitors with their identity) · *seat* `layout` (r/a: list, save, apply an autorandr profile) · `set-mode` (a: one output's resolution, rate, rotation, scale) · `primary` (a) · `dpi` (r/a) · `input-devices` (r) · `input-set` (a: touchpad tap, natural scroll, pointer speed) · `keyboard` (r/a: layout and options) · `screenshot` (d: one screen or all, as a file) · `x-log` (r: the server's errors since start) |
|
||||
| `i3` (*node-display-session*) | *seat* `reload` (a) · *seat* `workspaces` (r) · *seat* `windows` (r: tree with classes, titles, workspaces) · `focus` (d: window or workspace) · `move` (d: window to workspace or output) · `layout-save` / `layout-restore` (d: a workspace's arrangement) · `exec` (d: start a program in the session) · `kill` (d) · `bindings` (r: every key binding and what it runs) · `config-check` (r: validate the composed configuration before a reload) · `marks` (r) · `scratchpad` (d) |
|
||||
| `sway` (*node-display-server*, *node-display-session*) | the same seat verbs over Wayland, plus `outputs` (r) and `idle-inhibitors` (r) |
|
||||
| `lemurs` (*node-login-manager*) | *seat* `sessions` (r: what the login screen offers) · *seat* `default-session` (r/a) · `logins` (r: who logged in when, from the journal) |
|
||||
| `xterm` (*node-terminal-emulator*) | *seat* `open` (d: a terminal, optionally running a command, in a directory) · `font` (r/a: face and size) · `colours` (r) |
|
||||
| `i3status-rust` (*node-bar*) | *seat* `reload` (a) · `blocks` (r: what the bar shows and each block's current value) · `block-run` (r: run one block once and answer its output) · `themes` (r) |
|
||||
| `picom` (*node-compositor*) | *seat* `restart` (a) · `rules` (r: opacity, shadow and blur rules in force) · `window-opacity` (d) · `toggle` (d: compositing off and on, for a game or a test) |
|
||||
| `rofi` (*node-launcher*) | *seat* `menu` (d: show a list, answer the chosen line: the dmenu-compatible command as a tool) · `applications` (r: the desktop entries it would offer) · `themes` (r) · `run` (d) |
|
||||
| `dmenu` (*node-launcher*) | *seat* `menu` (d) |
|
||||
| `dunst` (*node-notifier*) | *seat* `send` (d: title, body, urgency, actions) · *seat* `history` (r) · `pause` / `resume` (d: do not disturb) · `close-all` (d) · `rules` (r) · `count` (r: shown, waiting, history) |
|
||||
| lock module (*node-lock-screen*) | *seat* `lock` (d) · `idle` (r/a: screensaver and display power timeouts) · `inhibit` (d: keep the screen on for a while) · `locked` (r: is the session locked now, and since when) |
|
||||
| clipboard manager (*node-clipboard*) | *seat* `history` (r: entries, newest first, length-limited) · *seat* `copy` (d: put text on the clipboard) · `paste` (r: what the clipboard holds now) · `clear` (d) · `delete` (d: one entry) |
|
||||
| `xclip` | `copy` (d) · `paste` (r): the plain clipboard without a manager |
|
||||
| `feh` (wallpaper) | `set` (d: an image, per output) · `current` (r) |
|
||||
| `fonts` | `families` (r: installed faces) · `match` (r: what `monospace`, `sans-serif` and `emoji` resolve to) · `glyph` (r: which installed font has a given character) · `cache-rebuild` (a) |
|
||||
| theme module | `appearance` (r/a: dark or light, for GTK, Qt and the portal at once) · `cursor` (r/a) · `icons` (r) · `portal-check` (r: which portal backend answers which interface) |
|
||||
| `gnome-keyring` (*node-secret-service*) | *seat* `unlocked` (r) · `lock` (d) · `collections` (r: names and item counts, never secrets) · `ssh-keys` (r: what the agent holds, by fingerprint) |
|
||||
| desktop hardware module (laptop) | `brightness` (r/a: panel and keyboard) · `battery` (r: charge, health, cycles, limit) · `charge-limit` (r/a) · `gpu-mode` (r/a: integrated, hybrid, discrete) · *seat* `profile` (r/a: quiet, balanced, performance) · `thermals` (r: temperatures and fan speeds) · `power-draw` (r) |
|
||||
|
||||
## The system and the account (027)
|
||||
|
||||
| module | tools |
|
||||
|---|---|
|
||||
| `docker` (*node-container-runtime*, ADR 0166) | *seat* `list`, `inspect`, `logs`, `stats`, `start`, `stop`, `restart` (r/a) · `images` (r: with size and which container uses each) · `prune` (a: dangling images, stopped containers not held by the mesh, build cache, with a dry run first) · `disk-usage` (r) · `networks` (r) · `volumes` (r: with what mounts each and whether the mesh holds it) · `events` (r: the last hour) · `daemon-config` (r) |
|
||||
| `docker-compose` | `projects` (r: compose projects running and where their files are) · `up` / `down` / `restart` (a: one project, by directory) · `logs` (r) · `ps` (r) |
|
||||
| `sudo` | `rules` (r: what the account may run, without a prompt and with one) · `check` (r: does the escalation the mesh relies on work here) |
|
||||
| `pacman` | `search` (r) · `installed` (r: with version and explicitly or as a dependency) · `info` (r) · `owns` (r: which package owns a path) · `files` (r) · `updates` (r: what an upgrade would change) · `upgrade` (a: with the news first) · `orphans` (r) · `remove-orphans` (a) · `cache` (r/a: size, clean to the last N versions) · `history` (r: installs and upgrades from the log) · `mirrors` (r/a: rank and refresh) · `news` (r: distribution news since the last upgrade) |
|
||||
| AUR (package repository, 027 question 1) | `search` (r) · `build` (a: on the build machine, into the mesh's repository) · `outdated` (r) · `published` (r) |
|
||||
| `snapd`, `flatpak` | `list` (r) · `install` / `remove` (a) · `update` (a) · `runtimes` (r) · `disk-usage` (r) |
|
||||
| `time-sync` | `status` (r: synchronised, offset, server) · `servers` (r) · `sync-now` (a) |
|
||||
| `localization` | `get` (r: locale, time zone, keymap) · `time-zone` (r/a) · `locales` (r) |
|
||||
| `kernel` | `running` (r: version, command line, uptime) · `installed` (r) · `modules` (r: loaded, with what uses them) · `reboot-needed` (r: a newer kernel or library than the one running) · `microcode` (r) · `boot-entries` (r) · `initramfs-rebuild` (a) · `dmesg` (r: errors since boot) |
|
||||
| `logrotate` | `status` (r: last rotation per log) · `force` (a: one configuration) · `big-logs` (r: the largest logs on the machine) |
|
||||
| `avahi` | `browse` (r: services on the local network) · `resolve` (r) |
|
||||
| `cups` | `printers` (r) · `queue` (r) · `cancel` (a) · `print` (a: a file to a printer) · `default` (r/a) |
|
||||
| `bluetooth` | `devices` (r: paired, connected, battery where reported) · `connect` / `disconnect` (a) · `scan` (r) · `power` (r/a) |
|
||||
| `ssh-client` (owns `~/.ssh`) | `hosts` (r: every `Host` and where it came from: the mesh, a module, the operator) · `check` (r: modes, keys without a passphrase, keys unused for a year, stale `known_hosts` entries) · `authorized` (r: who may log in, by fingerprint and comment) · `revoke` (a: one authorized key, into the operator's region) · `known-host` (r/a: verify, refresh one host's key) · `test` (r: can this machine reach a host and authenticate, batch mode) |
|
||||
| `sshd` | `sessions` (r: who is logged in, from where) · `config-effective` (r: `sshd -T`) · `failed-logins` (r: since a time, with fail2ban's verdicts) |
|
||||
| scripts modules | `list` (r: each script with its one-line description) · `run` (a: one script by name with arguments, as the account, bounded like `execute`) · `which` (r: which module ships a command) |
|
||||
| `node-env` (*node-environment*) | `show` (r: every variable and `PATH` entry with the module that contributed it) · `diff` (r: what a shell actually has versus what the mesh composed) |
|
||||
| `zsh` (*node-login-shell*) | *seat* `execute` · `zsh_config` (r) · `history-search` (r: the account's history, by pattern) · `functions` (r: aliases and functions in force, with where each came from) · `startup-time` (r: how long an interactive shell takes to start, per slot) |
|
||||
| `memory-pressure` | `status` (r: memory, swap, compressed swap ratio, pressure stall) · `top` (r: the largest processes) · `oom-history` (r: what was killed, when) |
|
||||
| `zfs` | `pools` (r: health, capacity, fragmentation) · `datasets` (r) · `snapshots` (r/a: list, create, destroy by name) · `scrub` (r/a: status, start) · `errors` (r) · `arc` (r: cache statistics) |
|
||||
| `nfs-server`, `samba` | `exports` / `shares` (r) · `clients` (r: who has it mounted now) · `reload` (a) |
|
||||
| `nfs-client`, `smb-client` | `mounts` (r: each share, mounted or not, and since when) · `mount` / `unmount` (a) · `test` (r: is the server reachable, is the export offered) |
|
||||
| hosts-file holder (*node-hosts-file*, ADR 0199) | *seat* `entries`, `add`, `remove` |
|
||||
| `vnstat`, `lm_sensors` | `traffic` (r: per interface, day, month) · `sensors` (r) |
|
||||
| mail consumer (future effort) | `accounts` (r) · `search` (r) · `unread` (r) · `read` (r: one message) · `mark` (a) · `send` (a) |
|
||||
|
||||
## What this catalogue is for
|
||||
|
||||
It is a starting list, not a contract. A tool becomes a contract only when it is a seat's verb, and
|
||||
each seat's verbs are decided in that seat's record (ADR 0132). A module's own tools can grow freely.
|
||||
Every row above is a tool the operator would otherwise run by hand over ssh. That is the measure of
|
||||
whether one is worth writing.
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
status: active
|
||||
initiated: 2026-10-04
|
||||
touches:
|
||||
- 02-DECISIONS/0165-container-runtime-is-what-a-machine-can-run-and-a-running-runtime-is-its-holders-health.md
|
||||
- 02-DECISIONS/0166-the-container-runtime-is-a-node-seat-and-the-host-creates-containers-through-its-holder.md
|
||||
- 02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md
|
||||
- 02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md
|
||||
- 03-DESIGN/01-to-be/37-the-operators-machine.md
|
||||
- 03-DESIGN/01-to-be/38-building-the-operators-machine.md
|
||||
became: []
|
||||
---
|
||||
|
||||
# 027 — The system layer as modules
|
||||
|
||||
## What is investigated
|
||||
|
||||
What runs on the machines below the operator's home and outside the mesh's own services, and which
|
||||
of it should be modules. That covers:
|
||||
|
||||
- the container runtime and its tools;
|
||||
- privilege (sudo);
|
||||
- the package manager and the software it cannot install;
|
||||
- time, locale, the kernel and boot;
|
||||
- log rotation;
|
||||
- the machine-specific daemons the workstations and servers carry: printing, bluetooth, VPN
|
||||
clients, virtualisation, storage, sharing.
|
||||
|
||||
## Why
|
||||
|
||||
The operator asked for the system level beside the graphical session. In particular:
|
||||
|
||||
- a `docker` module (decided in principle by the proposed ADRs 0165 and 0166, never built);
|
||||
- a `docker-compose` module for development work, assigned **only to the two workstations**.
|
||||
|
||||
Measured in [01](01-what-the-machines-run.md): on four machines, almost nothing at this level is
|
||||
owned by a module. The pieces differ by machine for no recorded reason. Three findings are security
|
||||
matters on their own.
|
||||
|
||||
## How it is approached
|
||||
|
||||
**Adopting is also improving** (the operator, 2026-10-04). A module is not a copy of what a machine
|
||||
does today. Making it is the moment to fix what is broken, drop what is dead, choose the better tool
|
||||
and remove the leftovers. Every module's design lists its improvements over today. **Every module
|
||||
also serves tools,** many of them, for reading, acting and diagnosing; a module that only places a
|
||||
package and a file is unfinished. The tools are catalogued in
|
||||
[026/05](../026-the-graphical-session-as-modules/05-the-tools-each-module-serves.md).
|
||||
|
||||
## What it touches
|
||||
|
||||
- **The container runtime seat** (ADRs 0165 and 0166, both proposed).
|
||||
- **The host's `package` shape**, which installs from the distribution's official repositories only,
|
||||
while the workstations carry 67 and 114 packages from elsewhere.
|
||||
- **How a secret reaches the account's environment.** ADR 0203 forbids it in the contributed
|
||||
environment, but a predecessor file supplies such secrets today.
|
||||
- **The facts the mesh assumes and never declares,** above all that the operator account escalates
|
||||
without a prompt.
|
||||
|
||||
## Documents
|
||||
|
||||
- [01 — What the machines run](01-what-the-machines-run.md): evidence.
|
||||
- [02 — Candidates and questions](02-candidates-and-questions.md)
|
||||
- [03 — The account's own tools](03-the-accounts-own-tools.md): `~/.ssh` as one module's, scripts on every machine, the keyring, the laptop's power management, mail as events
|
||||
- The predecessor's lessons, shared with research 026: [026/03](../026-the-graphical-session-as-modules/03-what-the-predecessor-taught.md)
|
||||
@@ -0,0 +1,103 @@
|
||||
# 01 — What the machines run
|
||||
|
||||
Measured 2026-10-04 on four machines, read-only, including the host's own record of what it applied:
|
||||
two servers (the anchor and a home server) and two workstations (a laptop and a desktop). "Owned"
|
||||
means a module the mesh assigns declares it.
|
||||
|
||||
## The container runtime
|
||||
|
||||
| | anchor | home server | laptop | desktop |
|
||||
|---|---|---|---|---|
|
||||
| docker | 29.8.2 | 29.8.2 | 29.7.2 | 29.7.2 |
|
||||
| compose | 5.5.1 | 5.6.0 | 5.5.0 | 5.5.0 |
|
||||
| buildx | 0.37.2 | — | — | — |
|
||||
| podman | — | 6.1.3 | 6.1.0 | 6.1.0 |
|
||||
| `docker.socket` | disabled | enabled | enabled | enabled |
|
||||
| `containerd.service` | disabled | disabled | disabled | **enabled** |
|
||||
| `daemon.json` beyond the shared keys | direct routing, two more insecure registries | log rotation (100 MB × 10) | — | — |
|
||||
| docker group | operator, **a CI user** | operator | operator | operator |
|
||||
|
||||
**Ownership:**
|
||||
|
||||
- The `docker` package is owned on one machine only, by the installer's bootstrap, not by a module.
|
||||
- `docker.service` is declared indirectly, by the name resolver and the private-network modules,
|
||||
which each merge their own keys into `daemon.json`.
|
||||
- Nothing owns the socket, containerd, compose, buildx or the group.
|
||||
|
||||
**Compose in use:**
|
||||
|
||||
- On the servers, no running container belongs to a compose project. Their compose files are
|
||||
pre-mesh trees under the operator's and root's homes, plus a dangling enabled unit for one of them.
|
||||
- On the workstations, compose runs development stacks, and pre-mesh service trees sit under a
|
||||
top-level directory.
|
||||
|
||||
The mesh marks its own containers with a host label. On the workstations, a handful of unlabelled
|
||||
development and test containers run beside its build agent.
|
||||
|
||||
## Privilege
|
||||
|
||||
- The operator account escalates **without a prompt on all four machines**. The mesh relies on this,
|
||||
but it is set by hand in `/etc/sudoers` (a `wheel` rule on two machines, the account named on
|
||||
two), and nothing declares it.
|
||||
- On the anchor, a **CI user from the predecessor** keeps passwordless sudo and docker membership,
|
||||
and a predecessor drop-in in `sudoers.d` survives.
|
||||
- On the desktop, the operator account is also in the **`root` group**.
|
||||
|
||||
## The package manager
|
||||
|
||||
- `pacman.conf` is stock except on one server (parallel downloads).
|
||||
- The mirror list was generated once by a tool that is no longer installed. On the anchor, it is the
|
||||
hosting provider's single mirror.
|
||||
- An AUR helper is installed everywhere.
|
||||
- **Packages from outside the official repositories:** 2 on the anchor, 21 on the home server,
|
||||
67 on the laptop, 114 on the desktop. They include:
|
||||
- the agent CLI, which a catalogue module declares as a package and the host cannot install;
|
||||
- a VPN client;
|
||||
- a remote-access client;
|
||||
- printer drivers;
|
||||
- GPU tools;
|
||||
- a kernel module built from source (DKMS) for a storage filesystem;
|
||||
- a snap daemon.
|
||||
|
||||
## Time, locale, kernel, boot
|
||||
|
||||
| | anchor | home server | laptop | desktop |
|
||||
|---|---|---|---|---|
|
||||
| time zone, keymap | **another zone**, a non-US console keymap | local zone, unset | local zone, unset | local zone, unset |
|
||||
| time sync | timesyncd plus a provider drop-in | timesyncd | timesyncd | **ntpd**, timesyncd disabled |
|
||||
| bootloader | grub (BIOS) | systemd-boot **and** grub | systemd-boot | systemd-boot **and** grub |
|
||||
| kernels | one | two, plus a DKMS filesystem module | one | one, plus a DKMS controller driver |
|
||||
| microcode | **none** | yes | yes | **none** |
|
||||
| swap | RAID partition | partition | zram, a file and a partition | partition |
|
||||
| log rotation timer | not found | enabled | not found | not found |
|
||||
|
||||
## Daemons and services no module owns
|
||||
|
||||
- **All four:** avahi.
|
||||
- **Workstations:**
|
||||
- a VPN client daemon (both);
|
||||
- virtualisation (incus) with a hand-made unit that inserts container-runtime firewall rules (both);
|
||||
- printing and bluetooth;
|
||||
- GPU and power tuning per model;
|
||||
- a remote-access daemon (laptop);
|
||||
- snap and flatpak (desktop);
|
||||
- the local model server, run from a hand-written unit although a catalogue module for it exists
|
||||
(desktop);
|
||||
- Samba sharing and a network filesystem mount from the home server (desktop). A second mount is
|
||||
failing, and its **credential is written in clear in `/etc/fstab`**.
|
||||
- **Servers:**
|
||||
- a storage pool (about 167 TB) with its import, mount and scrub units, an NFS server and Samba
|
||||
sharing (home server);
|
||||
- traffic and sensor monitoring (home server);
|
||||
- a DHCP client daemon the catalogue has a module for but does not assign there (home server);
|
||||
- cron, an entropy daemon, and the **legacy `iptables` services**, which run beside the mesh's own
|
||||
filter (anchor).
|
||||
- **Not found anywhere:** a backup agent, a monitoring agent, a second VPN mesh.
|
||||
|
||||
## What is plain debris
|
||||
|
||||
- Dangling enabled-unit links on three machines.
|
||||
- Predecessor blocks in `/etc/hosts` on both servers.
|
||||
- The CI user, and the predecessor sudoers drop-in, on the anchor.
|
||||
- Pre-mesh compose trees on the anchor, the home server and the desktop.
|
||||
- Unlabelled test containers on the workstations.
|
||||
@@ -0,0 +1,128 @@
|
||||
# 02 — Candidates and questions
|
||||
|
||||
## Decided by the operator on 2026-10-04
|
||||
|
||||
- **`docker`** holds the container runtime seat on every machine, as ADRs 0165 and 0166 propose. Those
|
||||
records are promoted from proposed when it is built.
|
||||
- **`docker-compose` is a module of its own,** the distribution's package and nothing else. It is
|
||||
assigned **only to the two workstations**, for development work. The servers run nothing through
|
||||
compose.
|
||||
|
||||
Later the same day, on the candidates below:
|
||||
|
||||
- **Yes, all of them:** `docker`, `docker-compose`, `sudo`, `pacman`, an AUR helper (question 1),
|
||||
`time-sync`, `kernel` (with boot and microcode), `logrotate`, `avahi`, `cups` with the printer's
|
||||
driver, and every server-only candidate.
|
||||
- **Locale, time zone and keymap are one module, `localization`.**
|
||||
- **`snapd` and `flatpak`** are modules, on the two workstations only.
|
||||
- **`incus` is the lab's,** whose module depends on it. It is not a module of its own beside the lab.
|
||||
- **The agent's and the local model server's modules are still being developed,** and are not
|
||||
assigned until they are.
|
||||
- **The predecessor's CI user is retired.** It was removed from the anchor the same day, with its
|
||||
sudoers line, its docker membership and a dangling unit link; the backup is on the machine.
|
||||
|
||||
## Candidate modules
|
||||
|
||||
**On every machine:**
|
||||
|
||||
| module | owns | first reason |
|
||||
|---|---|---|
|
||||
| `docker` | the packages (runtime, containerd), the service and socket, `daemon.json`'s base keys (live restore, log rotation), the docker group's members | four machines, four configurations, one owner on one |
|
||||
| `sudo` | the operator account's escalation as a drop-in, declared | the mesh's tools rely on it (to-be 38 WP4) and nothing states it |
|
||||
| `pacman` | `pacman.conf`'s few keys, the mirror list and its refresher, cache cleaning | mirrors generated once and never again |
|
||||
| `time-sync` | timesyncd and its drop-ins | two daemons across four machines |
|
||||
| `localization` | locale, time zone, console keymap (one module, the operator's choice) | one machine differs, with no record why |
|
||||
| `kernel` | the kernel packages, microcode, initramfs presets | two machines without microcode |
|
||||
| `logrotate` | the timer and the base configuration | rotation runs on one machine of four |
|
||||
| `avahi` | the daemon and name-service switch entry | on all four, owned by none |
|
||||
|
||||
**On the workstations only:**
|
||||
|
||||
- `docker-compose`;
|
||||
- `lemurs`, the login manager (research 026);
|
||||
- a VPN client module;
|
||||
- `incus` with its forward unit (the lab module declares the package on one workstation only);
|
||||
- `cups` with the printer's driver;
|
||||
- `bluetooth`;
|
||||
- per-model **hardware** modules: GPU, power, vendor keys, brightness. These are the same modules
|
||||
research 026 needs for the desktop's fragments.
|
||||
|
||||
**On the servers only:**
|
||||
|
||||
- `zfs` with its scrub timer, and the long-term kernel it builds against;
|
||||
- `nfs-server`;
|
||||
- `samba`;
|
||||
- `vnstat`, `lm_sensors`.
|
||||
|
||||
`cron` on the anchor serves one stock file and can go. So can the entropy daemon on a modern
|
||||
kernel.
|
||||
|
||||
**Retire, not model:** the legacy `iptables` services on the anchor. They duplicate the mesh's
|
||||
filter, which is ADR 0100's ground.
|
||||
|
||||
## Questions this effort has to answer
|
||||
|
||||
1. **Software outside the official repositories.** The host's `package` shape installs from the
|
||||
official repositories only. A catalogue module already declares an AUR package (the agent CLI),
|
||||
which no machine could install, and the workstations carry 181 such packages between them.
|
||||
|
||||
| | option | for | against |
|
||||
|---|---|---|---|
|
||||
| P1 | ADR 0205's pinned vendored archive, per piece | exists | wrong for packages that build native code or kernel modules |
|
||||
| P2 | **The build machine builds AUR packages into a package repository the mesh serves** from its artifact store. The host then installs them as packages, signed | one shape for every package; pinned, reviewed and built once | a repository to serve and a signing key to keep |
|
||||
| P3 | An AUR helper on each machine, driven by the host | nothing to serve | builds on every machine, unpinned: the predecessor's `git clone` in another form |
|
||||
|
||||
Starting position: P2, for anything with native code. ADR 0205 stays for plain files such as a
|
||||
theme.
|
||||
|
||||
2. **Secrets in the account's environment.** A predecessor file feeds package-registry and API tokens
|
||||
to the session. ADR 0203 refuses secrets in contributed values, because they travel in the clear.
|
||||
The candidate is ADR 0182's third class: a module's own process writes a mode-0600 file of
|
||||
exports, from secrets the vault hands it over the bus, and the shell and the session source it.
|
||||
This needs its own record.
|
||||
|
||||
3. **Per-machine sizing and drivers.** The swap layout, the GPU, the storage pool and the boot
|
||||
loader are facts of one machine's hardware. They belong in hardware modules, or in settings
|
||||
(issue 168), not in the shared ones.
|
||||
|
||||
4. **What a module may leave behind.** Compose is installed on both servers, unowned. The mesh
|
||||
removes nothing it did not make. The choice is between an operator's one-off removal and a
|
||||
server-side `absent` declaration.
|
||||
|
||||
5. **The hosts file.** ADR 0199 (decided on an open change, not yet merged)
|
||||
gives `/etc/hosts` to one module through a seat, `node-hosts-file`, with an operator region and
|
||||
three verbs. It is not built. Today the private network's foundation writes only its own block, and
|
||||
the rest of each file is a predecessor's stale blocks (both servers) or the operator's development
|
||||
names (both workstations). The candidate module is that seat's first holder. It takes the
|
||||
private-network block as a contribution, and its operator region replaces the hand-kept lines.
|
||||
|
||||
6. **Mounts.** The host has no shape for a filesystem mount; ADR 0091 is about what a container
|
||||
mounts. One workstation mounts a share of the home server over NFS, and a second share over SMB.
|
||||
That second one fails, and its credential sits in clear in `/etc/fstab`.
|
||||
|
||||
| | option | for | against |
|
||||
|---|---|---|---|
|
||||
| M1 | **A module owns `/etc/fstab`** and other modules contribute lines | one file, as people know it | the file also carries the root and boot filesystems the installer wrote, which no module should rewrite; a slot contribution into a file that can stop a machine booting |
|
||||
| M2 | **Each client module writes its own systemd mount (and automount) unit**, which is the service manager's drop-in for exactly this. The `nfs-client` or `smb-client` module declares the unit file and the service shape enables it. `/etc/fstab` stays the machine's | no new host shape, and no shared file; the unit names its own dependencies (network online, the private network) and an automount does not hang a boot when the server is away; unassigning removes the mount | a mount reads as a unit, not a line |
|
||||
| M3 | A new `mount` shape in the host | the host knows what a mount is | a second way to say what M2 says |
|
||||
|
||||
Starting position: **M2.** The credential an SMB mount needs is a secret, written by the module's
|
||||
own process from the vault, mode 0600, which is question 2's mechanism. The pair is a server
|
||||
module exporting (`nfs-server`, `samba`) and a client module mounting. The client requires the
|
||||
share the server provides, so the mount is resolved, not hand-typed.
|
||||
|
||||
7. **Two DHCP clients on one interface.** The home server runs `dhcpcd`, a DHCP *client* (no machine
|
||||
runs a DHCP server), next to the network manager, which is its assigned networking module. Both
|
||||
lease an address on the same interface, which therefore carries two LAN addresses. The catalogue's
|
||||
`dhcpcd` module is assigned nowhere, and this unit is a leftover. The network manager is the
|
||||
machine's one DHCP client, and `dhcpcd` should be disabled there.
|
||||
|
||||
## Security findings, independent of any module
|
||||
|
||||
1. A filesystem credential in clear text in a workstation's `/etc/fstab`, for a mount that is failing
|
||||
anyway.
|
||||
2. A predecessor CI user with passwordless sudo and docker membership on the anchor, and a
|
||||
predecessor sudoers drop-in. *The user was removed on 2026-10-04; the drop-in remains.*
|
||||
3. The operator account in the `root` group on one workstation.
|
||||
|
||||
Each is one small change. None waits for a module.
|
||||
@@ -0,0 +1,169 @@
|
||||
# 03 — The account's own tools: ssh, scripts, mail
|
||||
|
||||
Three further directions from the operator on 2026-10-04. Each is account-level, like the shell
|
||||
([to-be 41](../../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md)).
|
||||
|
||||
## `~/.ssh` is one module's
|
||||
|
||||
*"A module owns `~/.ssh`, so it is its responsibility that every folder is set up consistently and
|
||||
correctly."*
|
||||
|
||||
**Measured:**
|
||||
|
||||
- The catalogue's `ssh-client` module owns the directory (mode 0700) and one region of
|
||||
`~/.ssh/config`: a `Host` block per machine of the mesh. It owns nothing else.
|
||||
- On one workstation, a predecessor's header, `Include` and hand-written host block sat **above** the
|
||||
mesh's region. ssh takes the first match, so the predecessor's entries were the ones in force, for
|
||||
the same machines. Removed on 2026-10-04.
|
||||
- On the control machine, two keys of a retired CI system were still in the operator's
|
||||
`authorized_keys`, able to log in as the operator. Removed the same day.
|
||||
- Permissions differ by file and by machine. Backups of the configuration lie beside it.
|
||||
|
||||
**Starting position:** `ssh-client` becomes the holder of everything under `~/.ssh`, classified as
|
||||
ADR 0182 asks:
|
||||
|
||||
| path | class | how |
|
||||
|---|---|---|
|
||||
| `~/.ssh/`, its mode, every file's mode | owned | the directory resource, plus a check verb that reports a file with the wrong mode |
|
||||
| `~/.ssh/config` | written into, the mesh's block **at the start** | the mesh's hosts win; the operator's lines after it are kept; an `Include config.d/*` line in the block |
|
||||
| `~/.ssh/config.d/<module>` | owned by the contributing module | ssh's own drop-in: a work module adds its forge's host there (research 026 C1) |
|
||||
| `~/.ssh/authorized_keys` | written into, the mesh's block | the operator's keys as the mesh records them, and nothing a retired system left. The operator's own lines are kept below the block |
|
||||
| `~/.ssh/known_hosts` | written into, the mesh's block | every mesh machine's host key, so the first connection never asks |
|
||||
| private keys | found | never read and never written by the mesh; a key the mesh should hand out comes from the vault, through the module's own process (ADR 0182, third class) |
|
||||
|
||||
The sshd module is the other half: the machine's side. It is already in the catalogue.
|
||||
|
||||
## Scripts on every machine, shared and machine-specific
|
||||
|
||||
*"All nodes should get some custom scripts, both node-specific and mesh-specific (shared)."*
|
||||
|
||||
**Measured:** the operator's script folder holds 64 entries plus 33 in its `bin/`. It is under no
|
||||
version control, and exists only where it was copied. It mixes three kinds:
|
||||
|
||||
1. scripts belonging to a module (the desktop's watchers, lock, menus; a laptop model's brightness);
|
||||
2. the operator's own tools;
|
||||
3. installers that modules have replaced.
|
||||
|
||||
**Starting position:**
|
||||
|
||||
- **The operator's scripts live in a repository of their own,** registered as any application is
|
||||
([ADR 0015](../../02-DECISIONS/0015-applications-live-in-their-own-repository.md)), built as archives,
|
||||
unpacked into a directory the module owns under the home. `bin/` goes on `PATH` through an
|
||||
environment contribution ([ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md)),
|
||||
and small functions go into the shell through a `shell` contribution
|
||||
([ADR 0204](../../02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md)).
|
||||
- **"Machine-specific" is said by assignment, never by naming a machine**
|
||||
([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md)). One repository
|
||||
holds several modules:
|
||||
- `scripts` (shared, on every machine);
|
||||
- `scripts-workstation`;
|
||||
- `scripts-media`;
|
||||
- and so on, each assigned where it applies.
|
||||
|
||||
A script that belongs to a piece of software or hardware moves into that module instead. A flavor
|
||||
inside one module is what [research 026/03](../026-the-graphical-session-as-modules/03-what-the-predecessor-taught.md)
|
||||
says not to repeat.
|
||||
- **A script can also be a tool.** A script with a one-line description is served by the node's
|
||||
runtime, so it can be called through the mesh on any machine that has it.
|
||||
- A script that needs a secret gets it through question 2's mechanism, never from a file of
|
||||
environment secrets.
|
||||
|
||||
## The keyring
|
||||
|
||||
*"A keyring is also a good thing to create a module for."*
|
||||
|
||||
**Measured on the two workstations, which both run GNOME Keyring:**
|
||||
|
||||
- **On one, the keyring unlocks at login.** The login manager's PAM service includes `login`, which
|
||||
carries `pam_gnome_keyring`.
|
||||
- **On the other, it does not.** The PAM line is only in the screensaver's service, so at session
|
||||
start the window manager runs a script that asks for the password a second time and unlocks the
|
||||
keyring with it.
|
||||
- **On both, the session's start script starts the daemon again** with the ssh and gpg components,
|
||||
and exports the ssh agent's socket. The keyring's current release serves the ssh agent through a
|
||||
separate per-user socket unit instead.
|
||||
|
||||
**Starting position:** a `gnome-keyring` module that holds a node seat, `node-secret-service` (the
|
||||
holder of the desktop's secret service; a password manager could hold it instead). It declares:
|
||||
|
||||
- the package;
|
||||
- its lines in the login manager's PAM file, written into, never over (ADR 0102), so login unlocks it
|
||||
on every machine;
|
||||
- the ssh agent's user socket, once user-scoped units ship;
|
||||
- the agent's socket path as an environment contribution, which needs a machine fact for the
|
||||
account's runtime directory. ADR 0203 forbids `$` in values, so `$XDG_RUNTIME_DIR` cannot be
|
||||
written in one.
|
||||
|
||||
The second unlock prompt and the second daemon start go away.
|
||||
|
||||
## Mail as events
|
||||
|
||||
*"Ideally a mail consumer with all my mail accounts configured, so my mail is recorded in the bus."*
|
||||
|
||||
**Measured:**
|
||||
|
||||
- The predecessor polled one work mailbox every minute. It **read an access token out of the mail
|
||||
client's process memory**, called a mail API with it, and raised a desktop notification per unread
|
||||
message. It worked only while the mail client ran, and stopped silently when the predecessor's units
|
||||
were retired.
|
||||
- Two further predecessor modules served mail tools, for one provider and for IMAP.
|
||||
- The mesh runs a mail server of its own for its domains.
|
||||
|
||||
**Not decided here; it needs an effort of its own.** The questions it would have to answer:
|
||||
|
||||
- **Accounts and how each authenticates:**
|
||||
- IMAP with an app password;
|
||||
- a provider's OAuth with a registered application;
|
||||
- the mesh's own mail server, which can publish delivery itself.
|
||||
|
||||
An employer's tenant may forbid registering an application at all.
|
||||
- **What the bus records:**
|
||||
- headers and a summary as events;
|
||||
- bodies and attachments in an object store the event points at;
|
||||
- retention, since mail is the most personal data the mesh would hold.
|
||||
- **What consumes it:** a notifier bridge to the desktop (the predecessor's notifications), search,
|
||||
an agent's context.
|
||||
- **Where it runs:** one long-running module, not per machine (ADR 0198).
|
||||
|
||||
The obvious first step is the mail server the mesh already runs.
|
||||
|
||||
## Power management on the laptop
|
||||
|
||||
*"Power management for the laptop."*
|
||||
|
||||
**Measured on the laptop** (a gaming model with a hybrid GPU):
|
||||
|
||||
- **The platform profile is driven by a vendor daemon** (`asusd`) and its CLI. The vendor CLI is
|
||||
now in the official repositories; the copy installed came from elsewhere. A predecessor script
|
||||
runs as a user unit and switches the profile every five seconds: quiet on battery, balanced on
|
||||
mains, performance above 50 % CPU.
|
||||
- **The hybrid GPU's mode** (now hybrid) is held by a second vendor daemon (`supergfxd`), which is
|
||||
**not** in the official repositories. Kernel-module options for the discrete GPU's power state
|
||||
and its suspend, hibernate and resume units are set by hand. Its own power daemon is masked.
|
||||
- **The battery charge limit is 80 %,** set by the vendor daemon.
|
||||
- **The lid and power key suspend.** The brightness key is ignored by logind and handled by the
|
||||
vendor-key path. Both are logind drop-ins.
|
||||
- **Memory pressure:** compressed swap in RAM (`zram`) beside a swap file and a partition;
|
||||
`systemd-oomd` with drop-ins; a predecessor *memory guard* user unit that notifies before the OOM
|
||||
killer acts.
|
||||
- `upower` runs. There is no `power-profiles-daemon`, `tlp`, `auto-cpufreq` or `thermald`, so nothing
|
||||
competes with the vendor daemon, by design.
|
||||
|
||||
All of it came from two predecessor modules, one of which was a laptop-model *flavor*. A desktop
|
||||
received part of it (research 026/01).
|
||||
|
||||
**Starting position:**
|
||||
|
||||
- **A hardware module per machine model** (here, the laptop's model). It holds the vendor daemon and
|
||||
its profile configuration, the GPU mode daemon (ADR 0205's case, or the build machine's package
|
||||
repository of research 027 question 1), the discrete GPU's module options and suspend units, the
|
||||
logind drop-ins, the battery charge limit, and the vendor keys and brightness. It is assigned to
|
||||
the one machine of that model, and to any second one later.
|
||||
- **The profile switching** moves from a polling script to the module's own long-running code
|
||||
(ADR 0198). It reacts to the power-supply change event instead of polling, and its thresholds
|
||||
become settings (issue 168).
|
||||
- **Memory pressure is not the laptop's alone.** `zram` and `systemd-oomd` with the notifier are a
|
||||
`memory-pressure` module, assigned wherever wanted. The swap layout stays the machine's (`kernel`
|
||||
module, question 3).
|
||||
- A **`node-power-profile`** seat (vendor daemon, or `power-profiles-daemon` on other hardware)
|
||||
gives the mesh one verb, `profile`, the same on every machine that has one.
|
||||
+2
@@ -9,6 +9,8 @@ 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
|
||||
|
||||
> **The mechanism changed — 2026-10-04, by [ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md).** Where this record calls a kept region *a marked block in which the operator's own lines are kept*, read the inverse, which is what the host built: the mesh's region is the marked block, and every line outside it is the operator's, kept byte for byte and given back when the module goes. The decision stands: a node varies a module by settings and by the operator's own lines, never by an edit.
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0011](0011-managed-files-are-generated-never-edited.md) says a managed file is derived and an
|
||||
|
||||
@@ -9,6 +9,8 @@ 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
|
||||
|
||||
> **The mechanism changed — 2026-10-04, by [ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md).** The seat is no longer declared by the shell modules (§1). It is `node-login-shell`, in the mesh's own seat set, which a shell module claims. Its holder also places the shell code other modules contribute, and sources the account's environment ([ADR 0203](0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md)). What stands: one holder per node, the login shell set by the `user` shape and given back, `execute` as the contract, and any node may call it.
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0040](0040-what-a-module-is.md) names the shell as its example of a *shared* seat: bash, zsh
|
||||
|
||||
+9
-3
@@ -9,6 +9,12 @@ extends: 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||
|
||||
# 181. The operator account is a node fact, and a home is a placement root
|
||||
|
||||
> **Progressive insight — 2026-10-04.** This record called a resource under a home *home-scoped*, and a
|
||||
> module that places one a *home-scoped module*. There is no such kind of module
|
||||
> ([ADR 0173](0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md) §2: a module is
|
||||
> what it declares), so the three places now say *a resource placed under a home* and *a module placing
|
||||
> files under a home*. What was decided is unchanged.
|
||||
|
||||
*Reconstructed. The controller shipped this on 2026-09-27 and
|
||||
[to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) recorded it as built without a
|
||||
decision behind it. This record states what was decided, from the code and the design, and adds the
|
||||
@@ -35,7 +41,7 @@ its home; the account and its home are machine facts a definition may name in a
|
||||
and content; a roster file may say it lives under the home, and is then rendered per node, placed under
|
||||
that node's account's home, owned by the account, and left out on a node with no account. On
|
||||
2026-10-02 **all four nodes of the live mesh carry an empty account**: the fact exists and nobody has
|
||||
stated it, so no home-scoped resource can land anywhere yet.
|
||||
stated it, so no resource placed under a home can land anywhere yet.
|
||||
|
||||
## Considered Options
|
||||
|
||||
@@ -68,7 +74,7 @@ account. A definition names the account and its home as machine facts, never as
|
||||
may say it is a home file and is then placed and owned the same way. The controller resolves both at
|
||||
composition, and the host chowns what it creates.
|
||||
|
||||
**A node with no account cannot carry a home-scoped resource, and says so.** A roster fact that lives
|
||||
**A node with no account cannot carry a resource placed under a home, and says so.** A roster fact that lives
|
||||
under the home is left out of that node's declaration rather than written to nowhere. A resource naming
|
||||
the account fact on such a node is refused at composition, naming the fact the machine does not have.
|
||||
A module that writes a person's files is thereby unassignable to a machine with no person on it, which
|
||||
@@ -80,7 +86,7 @@ anything.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **The operator states the account before any home-scoped module lands.** Today none is stated, so the
|
||||
- **The operator states the account before any module placing files under a home lands.** Today none is stated, so the
|
||||
first assignment of such a module begins with four node records.
|
||||
- The roster carries each node's account, so a composed ssh configuration logs in as the right person
|
||||
on every machine — the gap that surfaced this, closed by the same fact.
|
||||
|
||||
+9
-3
@@ -9,6 +9,12 @@ extends: 02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-
|
||||
|
||||
# 182. Inside a home, the mesh owns the directory and the files it places, writes into the tool's own files, and holds everything else as found
|
||||
|
||||
> **Progressive insight — 2026-10-04.** This record said *a home-scoped module* and *the family of
|
||||
> home-scoped modules*. There is no such kind of module
|
||||
> ([ADR 0173](0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md) §2), and the rule
|
||||
> is about a directory under a home, whichever module declares it; the three places now say so. The
|
||||
> decision, its options and its consequences are unchanged.
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0181](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md) lets a module
|
||||
@@ -35,7 +41,7 @@ use tools that no longer exist. Nothing owns them; nothing will ever rewrite or
|
||||
`~/.ssh`: the mesh owns the directory and the files it places; it holds the person's private keys and
|
||||
personal drop-ins as found. That was argued from the lockout `~/.ssh` can cause. The argument here is
|
||||
the same shape with a different stake — the person's work rather than the person's way in — and it has
|
||||
to hold for every directory the family of home-scoped modules will touch, so it is a rule, not a
|
||||
to hold for every directory under a home that any module will touch, so it is a rule, not a
|
||||
section.
|
||||
|
||||
## Considered Options
|
||||
@@ -52,7 +58,7 @@ section.
|
||||
|
||||
## Decision
|
||||
|
||||
**A home-scoped module owns the directory it declares: its existence, owner and mode.** The host creates
|
||||
**A module that declares a directory under a home owns that directory: its existence, owner and mode.** The host creates
|
||||
it if absent, owned by the account, and never removes it while it holds anything
|
||||
([ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)). Inside it, every path the module touches
|
||||
is in exactly one of four classes, and **the class is visible in the definition from the shape
|
||||
@@ -105,7 +111,7 @@ finished its definition.
|
||||
| An owned file found with no record is kept once, then written | host tests of ADR 0102's kept-original rule |
|
||||
| Only the declared keys of a written-into file change, and are given back | host tests of ADR 0102: declared keys set, the rest kept, restored when undeclared |
|
||||
| Nothing found is touched | the family's lab check: a machine with a seeded home holding a person's file beside a predecessor's; after apply the person's file is byte-identical, the predecessor's is kept as the original, the mesh's keys are set and the person's keys in the same file remain; after unassign the mesh's files are gone, the keys are restored, the person's files are untouched and the directory stands |
|
||||
| Every path a home-scoped module touches is classified | a catalogue review rule for this family: each path is a directory, a file, a file written into, a secret-and-step, or absent — the first module written to it is [to-be 36](../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md) |
|
||||
| Every path a module touches under a home is classified | a catalogue review rule for this family: each path is a directory, a file, a file written into, a secret-and-step, or absent — the first module written to it is [to-be 36](../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md) |
|
||||
|
||||
## References
|
||||
|
||||
|
||||
+24
@@ -152,6 +152,30 @@ the node is bound to, and refuses with a notification otherwise.
|
||||
| An unservable binding refuses rather than lends | a manager test: a worker bound to a dead licence is answered with a refusal, never another licence's token |
|
||||
| A switch through the console changes the token on the node and nothing in the answer is a token | a live check on one workstation |
|
||||
|
||||
> **The mechanism changed — 2026-10-03, by [ADR 0193](0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md)
|
||||
> and [ADR 0198](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md).**
|
||||
> What stands: one manager holding the seat, one rotation source, a token sealed to the receiving
|
||||
> module's key on request/reply and never an event, the agent module alone writing what the agent
|
||||
> reads, the identity guard, the host knowing nothing. What moved: both modules' code is bundles the
|
||||
> node's runtime launches over stdio and is the bus for — `mesh/ask` for a call made on the module's
|
||||
> behalf, `mesh/publish` and `mesh/subscribe` beside it — so neither holds a bus credential of its own.
|
||||
> The manager's refresh and visits are a long-running bundle the control node's runtime launches. And,
|
||||
> by the operator's direction, **the manager starts every exchange**: it asks each bound node's agent
|
||||
> module for its public key, hands it a token, asks it for a login waiting to be adopted, and reconciles
|
||||
> every node on a schedule — which is what "the agent module asks the seat for its current token" and
|
||||
> "offers the grant to the manager" in the decision above now mean in practice. The agent module could
|
||||
> ask through its runtime; it does not need to.
|
||||
|
||||
> **The mechanism changed — 2026-10-04, by [ADR 0206](0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md).**
|
||||
> What stands: the manager holding the seat, one rotation source, the grants encrypted in its store, a
|
||||
> token sealed to the receiving module's key on request/reply and never an event, the agent module alone
|
||||
> writing what the agent reads, the identity guard, bindings as a person's act. What moved: the dated note
|
||||
> above — the manager no longer starts every exchange. Each node reports what it holds as state, without
|
||||
> the secret; the manager asks a node for its grant only when a report shows one it does not hold, adopts
|
||||
> a licence by refreshing it rather than into a licence configured beforehand, and keeps what each
|
||||
> consumer should hold as state, from which the node fetches its token by request. The rotation and switch
|
||||
> events are gone.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0024](0024-model-access-is-a-provision.md), [ADR 0050](0050-model-access-is-vendor-agnostic.md) — the licence as a named thing, the carve-out this moves with the manager
|
||||
|
||||
@@ -107,6 +107,16 @@ unreferenced.
|
||||
- The store is briefly unavailable each night, for as long as collection takes. Everything that
|
||||
pulls from it retries; nothing in the mesh treats a momentary store as a failure
|
||||
([ADR 0185](0185-a-control-plane-behind-its-seats-row-serves-what-it-can.md)).
|
||||
- **An apply arriving during the window reopens it**, because the host's rule for a container it
|
||||
finds stopped is to replace it, and `while-stopped` is the first thing that makes a stopped
|
||||
container intentional. Found by reading this before it merged, recorded as
|
||||
[issue 224](../04-ISSUES/224-an-apply-reopens-a-maintenance-window-by-recreating-what-it-held-still/00-report.md)
|
||||
rather than fixed here: the two candidate fixes — the window takes the apply lock, or the apply
|
||||
learns which containers are held — are each a decision with its own cost, and neither belongs
|
||||
inside this record. Nothing is worse than it was; the store has never collected at all.
|
||||
- **The sweep is bounded**: at most two hundred artifacts and sixty seconds per build, stopping at
|
||||
the first refusal, because it runs inside somebody's build. What is left over is offered again
|
||||
next time. The store stops growing from the first sweep; it does not empty in one.
|
||||
|
||||
## How this is checked
|
||||
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-10-04
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md
|
||||
---
|
||||
|
||||
# 201. A module keeps its current state in key-value buckets it declares, and reaches them through the runtime
|
||||
|
||||
## Context
|
||||
|
||||
A module's code reaches the bus through the node's runtime: it publishes events, subscribes to them
|
||||
and asks tools ([ADR 0198](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)).
|
||||
Events are kept for a week and replayed to a consumer that was away; requests are kept nowhere. What
|
||||
neither gives is **the current value of something**, seen by every machine, including one that joins
|
||||
after it was written. The first module to need it — the operator's agent on a machine — registers MCP
|
||||
servers for every machine as events, and a machine assigned later never hears of them; and it would
|
||||
replay a week of licence rotations where it needs only the binding that holds now. Research
|
||||
[024](../01-RESEARCH/024-state-a-module-keeps-on-the-bus/00-overview.md) measured the alternatives and
|
||||
the grants against a real server.
|
||||
|
||||
[Design 32](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §4 already names *state* as one of the
|
||||
mesh's relationships — 1:1, last per subject — and reserves it to the mesh's own declarations.
|
||||
[Design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §1 expects key-value buckets on the bus.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Key-value buckets a module declares, created by the controller, reached through the runtime.**
|
||||
Chosen.
|
||||
2. **State as events on EVENTS, read last-per-subject.** Rejected: retention is per stream and EVENTS
|
||||
keeps seven days, so a value unchanged for a week disappears; a second stream over the same subjects
|
||||
is refused by the server (design 32 §3). And events give no get, list or delete.
|
||||
3. **A last-per-subject stream per module, written by hand.** Rejected: it is what a key-value bucket
|
||||
is on the server, without the client's get, list, delete and watch — the mesh writing NATS's
|
||||
key-value layer again.
|
||||
4. **State in a module's own files or database, shared by asking a tool.** Rejected for state every
|
||||
machine must see: a machine joining later has to know whom to ask and poll, and an owner that is
|
||||
down answers nothing — the property the bus exists to remove.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. A module declares its state by name.** `state` names the buckets it owns, by local name; every
|
||||
instance of the module may write and read them. `reads` names another module's bucket as
|
||||
`<module>.<name>`, read-only. A bucket's options are its owner's: how many past values a key keeps,
|
||||
and how long a value lives. A manifest names no bucket, stream or subject (design 32 §1).
|
||||
|
||||
**2. One bucket per module per name, mesh-wide.** A key may name a machine by the module's own
|
||||
convention; the mesh does not scope buckets per machine.
|
||||
|
||||
**3. The controller creates the buckets, from the catalogue, on every raise** — from registration,
|
||||
like a seat's stream, so a reader can watch a bucket whose owner is not yet assigned anywhere. A module
|
||||
never creates one. The runtime's grant on each bucket is the union of what its carried modules may do:
|
||||
an owner's instances write and read, a reader's read.
|
||||
|
||||
**4. Each assignment is issued its buckets in its membership** ([ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)),
|
||||
by the name the module uses for each and whether it may write. The runtime serves `mesh/state.get`,
|
||||
`put`, `delete`, `keys` and `watch` on the bundle's channel from that list, and refuses — with the
|
||||
reason — a bucket the module was not issued and a write to one it only reads. A watch delivers the
|
||||
current values first, without deletions, then an end-of-current marker, then every change, each as a
|
||||
`mesh/state` request the bundle answers.
|
||||
|
||||
**5. No secret is stored in a bucket, sealed or not.** A bucket is a stream, and design 32 §10 keeps
|
||||
every secret off streams. A value that needs a secret names it; the secret travels on request/reply.
|
||||
|
||||
**6. The mesh caps size; a bucket outlives its module.** One value per key and no expiry unless the
|
||||
owner says otherwise; at most 256 KiB a value and 64 MiB a bucket. Unassigning a module leaves its
|
||||
buckets and what is in them ([ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)); a bucket
|
||||
whose declaration is gone from the catalogue is reported, never removed by the mesh.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A machine that joins reads the current state at once, and every machine sees a change as it
|
||||
happens, with no consumer created per reader and nothing replayed.
|
||||
- The runtime's channel has a sixth verb family, and the SDKs a small state surface over it — a
|
||||
contract, which ADR 0039 admits: it changes when the verbs do, rarely, and every module should be
|
||||
rebuilt when it does.
|
||||
- What got harder: the runtime must keep each module to its own buckets, because one principal per
|
||||
machine carries all of them and the server enforces only the union. A write the server refuses
|
||||
surfaces to a client as a timeout, not a refusal, so the runtime's own refusal is what a module sees.
|
||||
- The secrets rule is only partly mechanical. Sealed values cannot be recognised; the runtime refuses
|
||||
a value with a field whose name says it is a credential, which catches the ordinary mistake and not a
|
||||
determined one. For the operator's agent this means an MCP server's authorisation header stays out of
|
||||
its bucket.
|
||||
- Buckets accumulate as modules come and go; that they are reported rather than removed is the price
|
||||
of not deleting data.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A manifest's state names are local, and a read names a bucket its owner declares | the catalogue's registration check, per manifest; a catalogue test that every `reads` whose owner is present names a bucket that owner declares |
|
||||
| Buckets exist for every declared state | the controller's raise asserts them idempotently; its test over a real bus |
|
||||
| Owners write, readers only read | the composer's test of the grants, per principal kind; the runtime's refusal test over a real bus |
|
||||
| A watch hands current values first, without deletions, then changes | the runtime's test over a real bus |
|
||||
| No credential-named field in a value | the runtime's refusal test |
|
||||
| Live | one module puts on one machine and another machine's watch sees it; a machine assigned afterwards reads it at start |
|
||||
|
||||
## References
|
||||
|
||||
- Research [024](../01-RESEARCH/024-state-a-module-keeps-on-the-bus/00-overview.md)
|
||||
- [Design 32](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §4 and §10, [design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §1 and §3
|
||||
- [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md), [ADR 0039](0039-what-the-sdk-holds-and-refuses.md),
|
||||
[ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md),
|
||||
[ADR 0198](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)
|
||||
+22
-4
@@ -7,11 +7,14 @@ reconstructed: false
|
||||
extends: 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
|
||||
---
|
||||
|
||||
# 201. A provider declares what it derives for each consumer, and the mesh tells both ends
|
||||
# 202. A provider declares what it derives for each consumer, and the mesh tells both ends
|
||||
|
||||
> Written as 0188 on 2026-10-02 and renumbered to 0201 on 2026-10-04: the record of the bundles
|
||||
> refactor took 0188 on main while this one waited in a pull request, and the mesh's own code now
|
||||
> cites that one. Only the number moved; the decision is the one taken on the 2nd.
|
||||
> **Written as 0188 on 2026-10-02, renumbered to 0201, and to 0202 on 2026-10-04.** Twice, for the
|
||||
> same reason twice: the bundles refactor took 0188 while this waited in a pull request, and the
|
||||
> key-value-buckets record took 0201 while this waited again. Both times the number was free when
|
||||
> it was chosen and taken by the time this merged. Only the number moved; the decision is the one
|
||||
> taken on the 2nd. The check that refuses two records sharing a number is what caught it, both
|
||||
> times — a number is how a record is cited, and three repositories cite this one.
|
||||
|
||||
## Context
|
||||
|
||||
@@ -94,6 +97,15 @@ a literal in a consumer's definition is not merely redundant — it is the one t
|
||||
disagree with what the provider will actually create. The three object-store consumers lose their
|
||||
hand-written bucket names in this change.
|
||||
|
||||
**5. A consumer that keeps several holders of one provision may not be served a derived value.**
|
||||
Each holder gets its own login, `…_<local>` ([ADR 0094](0094-a-module-may-hold-several-secrets-from-one-provider.md)),
|
||||
and a provider derives from the login — so it would make one resource per holder, while the
|
||||
consumer's side has one binding and one `${bound:<provision>:<key>}`, both derived from the
|
||||
un-suffixed identity. That is this record's own failure one case to the side, and just as quiet:
|
||||
the consumer would authenticate and be refused on every object. Refused at resolution, naming
|
||||
both ends. Lifting it means giving the consumer's side a local dimension, which is a decision and
|
||||
not an omission.
|
||||
|
||||
## Consequences
|
||||
|
||||
- One more thing a definition may say, and one less thing a module may be wrong about. The
|
||||
@@ -105,6 +117,10 @@ hand-written bucket names in this change.
|
||||
- A provider that already serves consumers keeps serving them: the derived value equals what the
|
||||
code derived, so no bucket, database or login changes name. This is a change of **who says it**,
|
||||
not of **what is said**.
|
||||
- A refusal here fails **that machine's push**, naming the definition, and nothing else. That is
|
||||
deliberate and is the opposite of a module quietly left out: a definition that transcribes
|
||||
somebody else's rule is wrong everywhere, not just here, and the loud failure is in front of
|
||||
whoever can fix it.
|
||||
- The mesh now holds a rule in another system's alphabet — one rule, `dns`, stated once. A second
|
||||
alphabet is a decision, not an addition: the cost of each is that the mesh must be right about
|
||||
somebody else's naming, and that cost is only worth paying where the mesh already mints the name.
|
||||
@@ -118,6 +134,8 @@ hand-written bucket names in this change.
|
||||
consumer — one test asserting the three agree, because agreeing is the whole point.
|
||||
- Two consumers of one provider on one machine get two different derived values, and neither gets
|
||||
the other's.
|
||||
- A consumer with several holders of a deriving provider is refused, with both ends named — the
|
||||
test asserts the refusal, not merely that something failed.
|
||||
- A catalogue-wide test refuses a consumer definition that writes a literal where its provider
|
||||
derives: the provider's `serves` names the key, so the catalogue can say which definitions
|
||||
transcribe one.
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-10-04
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 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](../01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md)
|
||||
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](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)),
|
||||
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](../03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md)).
|
||||
- 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](../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)
|
||||
closes, a value a person varies becomes a setting of the module that contributes it
|
||||
([ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)).
|
||||
|
||||
## 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
|
||||
|
||||
- [Research 025](../01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md)
|
||||
- [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md),
|
||||
[ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md),
|
||||
[ADR 0177](0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md),
|
||||
[ADR 0182](0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
||||
- [To-be 41](../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md)
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-10-04
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md
|
||||
---
|
||||
|
||||
# 204. A module contributes shell code to the login shell in named slots, and the login shell is the mesh's seat
|
||||
|
||||
## Context
|
||||
|
||||
Some of what a shell runs at start is code in that shell's own syntax, and it belongs to other
|
||||
modules:
|
||||
|
||||
- a prompt theme loads itself and its configuration;
|
||||
- plugins load themselves;
|
||||
- a version manager sources its loader.
|
||||
|
||||
Order matters: a prompt's instant-prompt cache must run first, and syntax highlighting last. The
|
||||
predecessor kept all of this in one file per machine, and installed the theme and plugins by cloning
|
||||
them in a hook.
|
||||
[Research 025](../01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md) measured that file
|
||||
as byte-identical on four machines. It carries:
|
||||
|
||||
- the shell's defaults;
|
||||
- code belonging to four other pieces of software;
|
||||
- a handful of the operator's own lines.
|
||||
|
||||
Nothing gave the other pieces a way in.
|
||||
|
||||
Two further facts bear on the seat itself:
|
||||
|
||||
- **The seat is declared by the zsh module** ([ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)
|
||||
§1, under [ADR 0126](0126-a-module-declares-its-own-seats.md)). The controller refuses a second
|
||||
module declaring a seat name, so fish or bash could only ever claim it, and the seat exists only
|
||||
while zsh's definition is registered.
|
||||
- **The kept region is the other way round.** [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)
|
||||
describes a kept region as a marked block holding the operator's lines. The host built the inverse:
|
||||
the mesh's region is the marked block, and every byte outside it is kept, verified unchanged, and
|
||||
given back when the module goes.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **A drop-in directory** that each module places a file in, and the shell sources. Rejected: every
|
||||
contributor names a path inside the shell module's territory
|
||||
([ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md)); order becomes a naming
|
||||
convention nothing checks; and nothing ties the file to the shell actually being the one it is
|
||||
written for.
|
||||
2. **Facts the holder renders**, through `contributes` / `receives`. Rejected: code is not a fact,
|
||||
and the holder would only paste it.
|
||||
3. **Code contributed for a named shell in a named slot, assembled by the controller into the
|
||||
holder's file.** This is what the controller already does for fail2ban jails: each module supplies
|
||||
text in the tool's own format, and the controller sorts and concatenates it into the holder's file
|
||||
without interpreting it. Chosen.
|
||||
|
||||
For order, numbers (`10`, `50`, `90`) were rejected: every contributor guesses one, and collisions are
|
||||
silent. **Three named slots** were chosen: `first`, `normal`, `last`.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. `node-login-shell` is a node seat in the mesh's own set,** with the verb `execute`. It replaces
|
||||
the module-declared `login-shell`. Everything else ADR 0176 decided stands: the holder sets the
|
||||
account's login shell through the `user` shape, `execute` is the contract, and any node may call it.
|
||||
A shell module claims the seat; none declares it.
|
||||
|
||||
**2. Any module contributes shell code with `shell`:** entries naming the shell they are for (`zsh`,
|
||||
`bash`, `fish`), the slot, and the code. The controller does not read the code.
|
||||
|
||||
**3. The holder places the code with placeholders** in its own files: `${shell:<shell>:<slot>}`. Each
|
||||
is filled with that shell's code for that slot, from every module on the node:
|
||||
|
||||
- ordered by module name;
|
||||
- each piece preceded by a line naming its module;
|
||||
- empty when nothing is contributed.
|
||||
|
||||
A shell-code placeholder in a module that does not claim `node-login-shell` is refused.
|
||||
|
||||
**4. The holder's duties, which are the seat's protocol:**
|
||||
|
||||
- Source the account's environment ([ADR 0203](0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md))
|
||||
from the startup file every start of that shell reads. For zsh that is `.zshenv`, which a script, a
|
||||
login and `execute` all read.
|
||||
- Write its interactive block at the **start** of the interactive startup file, so the operator's own
|
||||
lines run after the mesh's and win.
|
||||
- Run `execute` as a non-interactive login shell in the account's home:
|
||||
- bounded below the runtime's call limit;
|
||||
- its output bounded;
|
||||
- its whole process group ended on timeout.
|
||||
|
||||
**5. The marked block is the mesh's; everything outside it is the operator's.** This is how ADR 0174's
|
||||
"kept region" is built. That record keeps its decision and gains a note saying where the mechanism
|
||||
lives.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A prompt, a plugin and a version manager are each a module with its own package or archive, its own
|
||||
configuration file, and a contribution. Assigning one adds its line to the shell, and unassigning it
|
||||
takes the line away at the next composition.
|
||||
- Assigning the shell module loses nothing the machine does today:
|
||||
- what is common to every machine becomes the shell module's default or another module's
|
||||
contribution;
|
||||
- what is the operator's stays below the block.
|
||||
- **The one-off migration is a person's act**, listed in the shell module's documentation
|
||||
([ADR 0182](0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)):
|
||||
delete the lines the block now carries from the found file.
|
||||
- `login-shell.execute` becomes `node-login-shell.execute`. Nothing has called it yet; the shell
|
||||
module was never assigned.
|
||||
- **What got harder:** a module wanting a line in the shell must say which shell and which slot, and a
|
||||
module supporting three shells writes its code three times. That is the honest cost of code in
|
||||
three syntaxes.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| Code lands in its slot, in module order, only for its shell | the controller's shell-contribution tests |
|
||||
| A shell-code placeholder outside the holder is refused | the catalogue check |
|
||||
| `node-login-shell` is the mesh's, and no module may declare it | the seat table's tests |
|
||||
| The zsh block sits at the start, sources the environment from `.zshenv`, and holds the three slots | the catalogue's zsh test |
|
||||
| `execute` is bounded in time and output and kills its process group | the zsh module's tool tests over real child processes |
|
||||
|
||||
## References
|
||||
|
||||
- [Research 025](../01-RESEARCH/025-how-a-module-plugs-into-the-shell/00-overview.md)
|
||||
- [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md),
|
||||
[ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md),
|
||||
[ADR 0203](0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md),
|
||||
[to-be 31](../03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md)
|
||||
- [To-be 41](../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md)
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-10-04
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
---
|
||||
|
||||
# 205. Software the distribution does not package ships as a pinned archive of the module's own
|
||||
|
||||
## Context
|
||||
|
||||
The prompt theme the operator uses is not in the distribution's repositories. Its two plugins and an
|
||||
autocomplete plugin are. The predecessor installed all four by running `git clone` against their
|
||||
upstream repositories from an install hook. That way:
|
||||
|
||||
- the version on a machine was whatever upstream's default branch held the day the hook ran;
|
||||
- two machines set up a week apart could differ;
|
||||
- a machine with no route to upstream failed its install.
|
||||
|
||||
The mesh already has a pinned, delivered form for a module's own files: an **archive artifact** built
|
||||
from a directory of the module's source, delivered by the artifact store, unpacked by the host's
|
||||
`archive` resource, and pinned by digest. One showcase module uses it.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Clone from upstream on the machine,** as the predecessor did. Rejected: unpinned, unreproducible,
|
||||
and it needs upstream reachable from every machine.
|
||||
2. **Build from the distribution's user repository.** Rejected: the host installs packages from the
|
||||
distribution's own repositories. A user-repository build is a toolchain on every machine for one
|
||||
theme.
|
||||
3. **Vendor a pinned upstream release into the module's directory and ship it as the module's archive
|
||||
artifact.** Chosen. The release and its version are named in the module, its licence travels with
|
||||
it, and every machine gets the same bytes from the mesh's own store.
|
||||
|
||||
## Decision
|
||||
|
||||
**A module whose software the distribution does not package carries a pinned upstream release in its
|
||||
own source directory and ships it as an archive artifact.**
|
||||
|
||||
- The module's documentation names the upstream, the version and the licence.
|
||||
- The host unpacks it with the `archive` resource into a directory the module owns.
|
||||
- An upgrade is a change to the module, reviewed like any other.
|
||||
|
||||
Software the distribution *does* package is installed as a package; a vendored copy of it is
|
||||
refused in review.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The catalogue grows by the size of what it vendors: 1.4 MB for the prompt theme at the pinned
|
||||
release.
|
||||
- Upstream's security fixes reach a machine only when somebody updates the module. That is the same
|
||||
trade every pinned dependency makes, and it is visible: the version is in the module.
|
||||
- **What got harder:** a vendored program that downloads more at run time, as the prompt theme does
|
||||
for its git status helper, still fetches that part from upstream on first use. This record pins
|
||||
what the mesh ships, not what the software fetches for itself. The module's documentation says so.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| The archive is pinned by digest | the host's declaration validation, which refuses an archive without one |
|
||||
| The upstream, version and licence are named | review of the module's documentation; the module's test asserts the licence file is in the archive |
|
||||
| Packaged software is not vendored | review |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md)
|
||||
- [To-be 41](../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md)
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-10-04
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
|
||||
---
|
||||
|
||||
# 206. A node reports the Anthropic grant it holds; the licence manager adopts a licence by refreshing it, and what each node should hold is the manager's state
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
|
||||
made the licence manager a module holding the `anthropic-licence-manager` seat: one rotation source, the
|
||||
long-lived grants in its own store, a short-lived token handed to a node sealed on request/reply, the
|
||||
agent module alone writing what the agent reads. How the manager *learns* a licence, and who starts each
|
||||
exchange, it left to a later shape, and three texts have since disagreed: ADR 0183 has a node register
|
||||
its key and the manager adopt a login only into a licence the node is already bound to; its dated note
|
||||
of 2026-10-03 has the manager start every exchange and visit every node on a schedule; the agent module
|
||||
as built asks the seat for its token when an event says to, and pushes a login to the seat.
|
||||
|
||||
**The operator settled it on 2026-10-04, in the operator's own words:** the manager must hold the active refresh token;
|
||||
whichever node a login happened on holds the latest one; every client publishes what its credentials
|
||||
file holds, the manager sees a licence it does not own yet and takes it into its store, and from then on
|
||||
rotates it and distributes the access token. A manager launched for the first time holds no licence and
|
||||
accepts what the clients report. Several nodes report the same account — today the nodes are all logged in
|
||||
to one personal account — and before the manager adopts a grant it must know the refresh token still
|
||||
works.
|
||||
|
||||
Two facts bound how that is built:
|
||||
|
||||
- **A refresh token cannot be published.** Anything published on the bus is kept, and a secret never
|
||||
enters a stream, sealed or not ([design 32](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §10).
|
||||
A module's state is a stream too, and the runtime refuses a value carrying a field named like a
|
||||
credential ([ADR 0201](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md);
|
||||
refused live on 2026-10-04 for an `Authorization` header).
|
||||
- **A refresh token can only be checked by using it.** No endpoint answers "is this refresh token
|
||||
valid" without exchanging it, and an exchange is presumed to rotate it (ADR 0183: the predecessor
|
||||
lost a licence to a reused one). Checking and adopting are therefore one act, and whoever checks
|
||||
becomes the token's only live holder.
|
||||
|
||||
Since ADR 0201 the bus has the shape this needs: **state** every node sees, including one that joins
|
||||
later or a manager that starts later, read whole on start and then watched.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Each node publishes its credentials file, the token included.** What the operator described,
|
||||
literally. Rejected for the token only: it would sit in a stream every principal that reads the
|
||||
bucket can read, for as long as the bucket keeps it, and the runtime refuses it anyway.
|
||||
2. **The manager visits every node on a schedule and collects a waiting login** (ADR 0183's dated
|
||||
note). Rejected: the manager must know every node in advance and poll it, a node that joins later
|
||||
waits for the next visit, and "what does each node hold" lives nowhere anyone can read.
|
||||
3. **Each node reports what it holds as state, without the secret; the manager asks for the secret
|
||||
only when the report shows a grant it does not hold, and adopts by refreshing.** Chosen: the
|
||||
operator's flow, with the one part that cannot be on the bus moved onto request/reply.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. Every agent module reports what its node holds, as its own state.** One key per node in the
|
||||
module's `holdings` state: the account's identity as the agent's own state file names it (account id,
|
||||
address, organisation), the kind, the refresh token's **fingerprint** and whether one is present at
|
||||
all, the access token's fingerprint and expiry, the licence it was last handed, and when the credentials
|
||||
file last changed. Written when the module starts — a node already logged in when the module is first
|
||||
assigned reports at once — and again whenever the credentials file changes. **No token, ever**: a
|
||||
fingerprint names a token without being one.
|
||||
|
||||
**2. A licence is an account, and the manager learns it from the reports.** The manager reads every
|
||||
node's `holdings` at start and watches them. A report carrying a refresh token whose fingerprint the
|
||||
manager does not hold is a **candidate**: for an account it has no licence for yet, a new licence; for
|
||||
one it has, a login made since. A manager launched for the first time holds no licence and treats
|
||||
every report as a candidate. An API key still enters only through the seat's `adopt` verb, from a file
|
||||
on the manager's node.
|
||||
|
||||
**3. The secret travels only when asked for.** For a candidate, the manager calls that node's agent
|
||||
module on request/reply, giving its own public key, and is answered with the grant sealed to that key
|
||||
(ADR 0183's channel, unchanged).
|
||||
|
||||
**4. Adopting is refreshing.** The manager exchanges the candidate's refresh token at the vendor's
|
||||
endpoint under its lease for that account. If the exchange succeeds, the grant it got back is the
|
||||
licence's, stored encrypted, and the manager is from then on its only rotation source. If it fails, the
|
||||
candidate is recorded dead, nothing is adopted, and the report says so. **Several nodes, one account:**
|
||||
candidates for one account are tried newest login first; the first that refreshes is adopted, and the
|
||||
manager does not exchange the others.
|
||||
|
||||
**5. A node holds an access token only, so the latest login wins.** A node bound to an adopted licence
|
||||
is handed the access token and nothing else, and the agent module writes the credentials file without a
|
||||
refresh token — so the agent on the node can never refresh it, and two refreshers never hold one grant.
|
||||
A refresh token appearing in a node's file afterwards can therefore only be a person's login there; its
|
||||
report makes it a candidate, and if it refreshes it replaces the licence's grant. That is the operator's
|
||||
"whichever node a login happened on holds the latest one", made mechanical.
|
||||
|
||||
**6. What each consumer should hold is the manager's state.** One key per consumer in the manager's
|
||||
`bindings` state: the licence, its kind, and a **generation** that increases with every rotation and
|
||||
every switch. The agent module watches its own key; when the generation is newer than the one it
|
||||
applied, it asks the seat's `current` verb for the token, sending its public key, and is answered sealed
|
||||
(request/reply). A node that was away reads its key when it is back and asks once. The `licence.rotated`
|
||||
and `licence.switched` events go: what they announced is now the state itself, and a node needs the
|
||||
latest, not the history.
|
||||
|
||||
**7. A first binding follows the login.** When the manager adopts a licence from a node's report, a
|
||||
node with no binding yet whose report names that account is bound to it. Every later change is a
|
||||
person's act through `bind`, `switch` and `release`, as ADR 0183 says.
|
||||
|
||||
**8. The identity guard stands, on two sources.** The account a grant is filed under is the identity
|
||||
the node read from the agent's own state. Where the vendor's answer to the refresh names the account,
|
||||
the manager compares the two and refuses a mismatch with a notification; whether it names it is
|
||||
measured when the manager is built, and the record of which source decided is kept in the audit.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The manager needs no configuration to start: launched on a mesh whose nodes are logged in, it adopts
|
||||
every account they hold, one licence each, from the newest login that still refreshes.
|
||||
- Every node's holding is readable by anyone allowed to read the state — the console, an agent, the
|
||||
operator — without a token in sight, which is what `licence_status` on each node answered one at a
|
||||
time.
|
||||
- **What got harder:** adoption consumes the refresh token the node held. On a node whose grant was
|
||||
adopted, the agent's own copy is dead from that moment; until the manager hands it an access token
|
||||
(decision 6), the agent keeps the access token it already had, which lives hours. And a node whose
|
||||
file still holds a refresh token after adoption — it was not handed one yet — is a second holder of a
|
||||
dead grant, not a live one, so the rotation-source rule holds.
|
||||
- A candidate whose refresh fails is not retried by the manager: a dead refresh token does not come
|
||||
back. A person logs in again, and the new report is a new candidate.
|
||||
- Nothing in the reports is secret, but they do say which account each node uses; readers of the state
|
||||
are declared in manifests like any other.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| No report carries a token | the runtime refuses a credential-named field (ADR 0201's test); the agent module's test: a report built from a full credentials file holds fingerprints and identity only |
|
||||
| A node already logged in reports at start | the agent module's test: with a credentials file present and unchanged, starting writes its `holdings` key |
|
||||
| A candidate is adopted only by a successful refresh, newest login first, once per account | the manager's tests against a stub vendor: two reports for one account, the newer refreshes and is adopted, the older is never exchanged; a failing refresh adopts nothing and records the candidate dead |
|
||||
| A node is handed an access token only | the agent module's test: the file it writes after a hand-over holds no refresh token |
|
||||
| A newer generation is fetched once, by request | the agent module's test: a `bindings` change with a newer generation asks `current` once; an equal one asks nothing |
|
||||
| No event carries a token, and none announces a rotation any more | the manager's test of everything it publishes |
|
||||
| Live | the manager launched with no licence on a mesh whose four nodes are logged in to one account adopts one licence, binds the four nodes, and each node's file then holds an access token and no refresh token |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) — the manager, its seat and its channel, which this extends
|
||||
- [ADR 0201](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md) — module state, and the refusal of a secret in it
|
||||
- [design 32](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §10 — no secret in a stream
|
||||
- [to-be 36](../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md), [to-be 39](../03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md) — the two modules, amended by this record
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-04
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
|
||||
---
|
||||
|
||||
# 207. A module depends on the node seats that apply its resources
|
||||
|
||||
## Context
|
||||
|
||||
A module declares resources: packages, services, containers, files. Some of those are applied
|
||||
through software on the machine that is itself a module:
|
||||
|
||||
- a service through the service manager;
|
||||
- a package through the package manager;
|
||||
- a container through the container runtime.
|
||||
|
||||
Until now nothing said so. A module carried a *capability* such as `service-manager` or
|
||||
`package-manager`, which the host detects on the machine. A capability says the software is
|
||||
installed. It does not say that a module of the mesh holds the role and answers for it.
|
||||
|
||||
The cost showed on 2026-10-04:
|
||||
|
||||
- **A networking module declared the service manager's own package.** The controller allows one
|
||||
declaration of a resource per node, so the module that *is* the service manager could not declare
|
||||
its package and had been written without it. The networking module's real relation to the service
|
||||
manager, that it needs one held on its node, was nowhere.
|
||||
- **The container runtime** has had this decided for its own case since ADR 0165 and ADR 0166
|
||||
(proposed): a module that delivers a container needs the runtime seat held on its machine, derived
|
||||
from the container resource, with no manifest field.
|
||||
- **The operator's order for building the machines' modules** (to-be 42) is *the most core first*.
|
||||
That is an order the mesh should enforce, not one a person should remember.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Keep capabilities as the only gate.** Rejected: a capability is a fact about the machine, not
|
||||
about the mesh. Software installed by hand satisfies it, and nothing then answers for it.
|
||||
2. **A manifest field per module naming the seats it needs.** Rejected: a module would restate what
|
||||
its resources already say, and a module that adds a service but forgets the field passes.
|
||||
3. **Derive the dependency from the resources,** as ADR 0165 already does for containers, and refuse
|
||||
an assignment whose seats are not held on the node. Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. Three node seats apply resources,** each in the mesh's own set:
|
||||
|
||||
| resource | applied through | seat | first holder |
|
||||
|---|---|---|---|
|
||||
| `service` | the service manager | `node-service-manager` (ADR 0177) | `systemd` |
|
||||
| `package` | the package manager | `node-package-manager` (new) | `pacman` |
|
||||
| `container` | the container runtime | `node-container-runtime` (ADR 0166) | `docker` |
|
||||
|
||||
`node-package-manager` is new and has no verbs yet. `node-container-runtime` is seeded now as ADR 0166
|
||||
names it. Its verbs, and the host creating containers through its holder, stay with that record's
|
||||
acceptance.
|
||||
|
||||
**2. A module depends on each seat its resources need.** The controller derives this from the
|
||||
resource types the module declares. A module never states it.
|
||||
|
||||
**3. A dependency is met when any module assigned to the same node holds the seat,** the module
|
||||
itself included. The holders of these seats declare resources of each other's kinds: the service
|
||||
manager's package needs the package manager, and the package manager's timer needs the service
|
||||
manager. They are therefore judged as the node's whole set of assignments, never one at a time.
|
||||
|
||||
**4. Where it is checked:**
|
||||
|
||||
- **At `assign`,** an assignment whose dependencies are unmet by the node's assignments, including the
|
||||
new one, is refused. The refusal names each seat and the modules in the catalogue that can hold it.
|
||||
- **At composition,** an unmet dependency on a node is **reported** in `status` until the three holders
|
||||
are assigned to every node. Then it is **refused** like any unresolved requirement. The switch is one
|
||||
line in the controller, made when `status` reports none.
|
||||
|
||||
**5. The mesh's own foundation is exempt.** These are the pieces genesis lays before any module exists:
|
||||
the host, the private network and the bootstrap runtime. Their declarations are the installation's,
|
||||
not a module's.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The order of to-be 42 becomes the mesh's: `systemd`, `pacman` and `docker` on a node before
|
||||
anything that installs, runs or contains.
|
||||
- **Two modules no longer declare one shared package to say they need it.** A component's module
|
||||
(networkd's) declares what it configures and depends on the seat. The component's own package
|
||||
belongs to the module that holds the seat.
|
||||
- Capabilities stay what they are, facts about the machine, used where a module needs the machine to
|
||||
be able to do something.
|
||||
- **What got harder:** a module can no longer be tried on a node that lacks the core three. That is
|
||||
the point.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| The dependency is derived from resources: a service, a package and a container each need their seat | the controller's resolve tests |
|
||||
| A node whose assignments hold the seats passes; one missing a holder is refused at `assign`, naming the seat and its possible holders | the same tests, and `assign` live |
|
||||
| Mutual dependencies among the holders resolve when they are assigned together | the same tests |
|
||||
| Until the switch, an unmet dependency is reported in `status` and does not refuse a push | the controller's status test |
|
||||
| Foundation declarations are exempt | the composition test with genesis's declarations |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0165](0165-container-runtime-is-what-a-machine-can-run-and-a-running-runtime-is-its-holders-health.md),
|
||||
[ADR 0166](0166-the-container-runtime-is-a-node-seat-and-the-host-creates-containers-through-its-holder.md),
|
||||
[ADR 0177](0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)
|
||||
- [To-be 42](../03-DESIGN/01-to-be/42-the-machines-modules-in-order.md)
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-10-04
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md
|
||||
---
|
||||
|
||||
# 208. The graphical session is one module per piece, on the mesh's seats
|
||||
|
||||
## Context
|
||||
|
||||
The two workstations run one predecessor desktop
|
||||
([research 026](../01-RESEARCH/026-the-graphical-session-as-modules/00-overview.md)). It was a module of
|
||||
983 lines with four flavors and 92 theme variables. One of its machines carried another machine model's
|
||||
hardware files, and its session's environment was a hand-kept copy of the account's. The operator asked
|
||||
for the desktop as modules at the shell's level, consistent across machines, with sway as a sibling of i3.
|
||||
To-be 38 named this WP7, and to-be 37 left one question for the resolver: how a module says it needs a
|
||||
display server held on its node.
|
||||
|
||||
## Considered Options
|
||||
|
||||
- **One desktop module, as before** (research 026 §1, G1). Rejected: flavors again, and the evidence is
|
||||
a flavor on the wrong machine.
|
||||
- **One module per piece of software.** Chosen.
|
||||
- **For "i3 needs an X server" (research 026 §3):**
|
||||
- a new field (R2), rejected as a second way to say what provisions already say;
|
||||
- assigning carefully (R3), rejected because that is the misassignment the evidence shows;
|
||||
- **a provision with the machine's reach** (R1), chosen.
|
||||
- **For other modules' lines in a holder's file (research 026 §5):**
|
||||
- only drop-ins (C1), rejected because two files the session needs have no drop-in convention;
|
||||
- only slots (C2), rejected as needless where the tool already reads a directory;
|
||||
- **both, the boundary drawn by the tool** (C3), chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. One module per piece of software:** `lemurs`, `xorg`, `i3`, `xterm`, `picom`, `rofi`, `dmenu`,
|
||||
`dunst`, the lock screen, `xclip`, the clipboard manager, `feh`, `i3status-rust`, a theme module,
|
||||
`fonts`, `gnome-keyring`, and later `sway`, `foot`, `waybar` and `mako`. No flavors. What follows a
|
||||
machine's hardware is that model's hardware module (research 027/03).
|
||||
|
||||
**2. The roles are node seats in the mesh's own set,** each with the verbs research 026/05 starts it with:
|
||||
|
||||
| seat | holders | verbs to start with |
|
||||
|---|---|---|
|
||||
| `node-login-manager` | lemurs | `sessions` |
|
||||
| `node-display-server` | xorg, sway | `displays`, `layout` |
|
||||
| `node-display-session` | i3, sway | `reload`, `workspaces`, `windows` |
|
||||
| `node-terminal-emulator` | xterm, foot | `open` |
|
||||
| `node-launcher` | rofi, dmenu | `menu`, the dmenu-compatible command |
|
||||
| `node-notifier` | dunst, mako | `send`, `history` |
|
||||
| `node-lock-screen` | the lock module, swaylock | `lock` |
|
||||
| `node-clipboard` | the clipboard manager | `history`, `copy` |
|
||||
| `node-bar`, `node-compositor` | i3status-rust, waybar; picom | none yet |
|
||||
| `node-secret-service` | gnome-keyring | none yet |
|
||||
|
||||
A compositor that is its own server holds two seats, as sway does.
|
||||
|
||||
**3. A display is a provision with the machine's reach.**
|
||||
|
||||
- A display server provides `x11-display` or `wayland-display`, reachable only on its own machine.
|
||||
- A module that draws on a display requires the one it speaks: i3, picom, xterm and the X lock require
|
||||
`x11-display`; sway's companions require `wayland-display`.
|
||||
- A requirement with the machine's reach is resolved on the requiring module's own node, or not at
|
||||
all, and is refused naming the seat's holders.
|
||||
- `xwayland`, as its own module, provides `x11-display` inside a Wayland session.
|
||||
|
||||
This answers to-be 37 §4 by reusing provisions and reach rather than a new field. A capability the host
|
||||
reports, `graphical-session`, still gates the display server itself.
|
||||
|
||||
**4. Other modules contribute to a holder's file in the tool's own grain.**
|
||||
|
||||
- Where the tool reads a directory, the contributor places its own file there:
|
||||
- i3's `include` directory;
|
||||
- dunst's `dunstrc.d`;
|
||||
- XDG autostart;
|
||||
- `environment.d`;
|
||||
- fontconfig's `conf.d`;
|
||||
- ssh's `config.d`;
|
||||
- the login manager's session directory.
|
||||
- Where it does not, **ADR 0204's slots serve beyond shells.** A `shell` contribution's `for` may also
|
||||
name:
|
||||
- `xinitrc`: POSIX code the session's start runs;
|
||||
- `xresources`: X resources merged at session start.
|
||||
|
||||
The holder of `node-display-server` places them with `${shell:xinitrc:<slot>}` and
|
||||
`${shell:xresources:<slot>}`.
|
||||
|
||||
**5. The display server's module writes the session's start.** It writes a block at the start of
|
||||
`~/.xinitrc`, in this order:
|
||||
|
||||
1. it sources the account's environment (ADR 0203);
|
||||
2. it imports the session's own variables into the user manager and D-Bus activation, by an explicit
|
||||
list;
|
||||
3. it merges the X resources;
|
||||
4. it runs the `xinitrc` slots;
|
||||
5. it ends by starting the session holder's command, which the session module contributes in the
|
||||
`last` slot.
|
||||
|
||||
The desktop's identity and the theme variables are environment contributions of the session and
|
||||
theme modules. The hand-kept environment in today's file goes, and so does the predecessor's file of
|
||||
secrets (research 027 question 2).
|
||||
|
||||
**6. Per-machine values:**
|
||||
|
||||
- Monitor layouts are profiles keyed by the monitors' identities (research 026/04). They are the
|
||||
operator's data, and the display server's `layout` verb manages them.
|
||||
- DPI and theme values are module defaults now, and settings after issue 168.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A workstation's desktop is a list of assignments, the same on both. The one machine model's
|
||||
hardware is one more assignment.
|
||||
- The X stack is built first, and sway is designed in from the start.
|
||||
- The controller learns:
|
||||
- the eleven seats;
|
||||
- provisions with the machine's reach;
|
||||
- two more names for a contribution's `for`.
|
||||
- **What got harder:** a module that draws must say which display it speaks, and one that wants both
|
||||
ships twice.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| The seats are in the mesh's own set, refused to any module that declares them | the seat table's tests |
|
||||
| A machine-reach requirement resolves on its own node only, refused naming the holders | the controller's resolve tests |
|
||||
| `xinitrc` and `xresources` slots are placed only by the display server's holder | the catalogue check |
|
||||
| The session block sources the environment, merges resources, runs the slots and ends with the session | the `xorg` module's manifest test, and on the proving workstation |
|
||||
|
||||
## References
|
||||
|
||||
- [Research 026](../01-RESEARCH/026-the-graphical-session-as-modules/00-overview.md), its 02, 04 and 05
|
||||
- [ADR 0203](0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md),
|
||||
[ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md),
|
||||
[ADR 0207](0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md)
|
||||
- [To-be 42](../03-DESIGN/01-to-be/42-the-machines-modules-in-order.md)
|
||||
@@ -190,7 +190,8 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0187** — [A dead tracker is not the machine's failure](0187-a-dead-tracker-is-not-the-machines-failure.md)
|
||||
- **0189** — [The store keeps what the records name, and a maintenance step holds its writers still](0189-the-store-keeps-what-the-records-name.md)
|
||||
- **0190** — [A seat's work is shared by its holders, and building is the first such role](0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md)
|
||||
- **0201** — [A provider declares what it derives for each consumer, and the mesh tells both ends](0201-a-provider-declares-what-it-derives-for-each-consumer.md)
|
||||
- **0202** — [A provider declares what it derives for each consumer, and the mesh tells both ends](0202-a-provider-declares-what-it-derives-for-each-consumer.md)
|
||||
- **0207** — [A module depends on the node seats that apply its resources](0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md)
|
||||
|
||||
### Its tiers, from the bottom up
|
||||
|
||||
@@ -300,6 +301,12 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0195** — [The mesh's tools are found by address, not announced whole](0195-the-meshs-tools-are-found-by-address-not-announced-whole.md)
|
||||
- **0197** — [Every tool announces itself on the bus, in the NATS services protocol](0197-every-tool-announces-itself-on-the-bus-in-the-nats-services-protocol.md)
|
||||
- **0198** — [A module's long-running code is launched by the node's runtime, and reaches the bus through it](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)
|
||||
- **0201** — [A module keeps its current state in key-value buckets it declares, and reaches them through the runtime](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)
|
||||
- **0203** — [The account's environment is one module's, and every module contributes to it](0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md)
|
||||
- **0204** — [A module contributes shell code to the login shell in named slots, and the login shell is the mesh's seat](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md)
|
||||
- **0205** — [Software the distribution does not package ships as a pinned archive of the module's own](0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md)
|
||||
- **0206** — [A node reports the Anthropic grant it holds; the licence manager adopts a licence by refreshing it, and what each node should hold is the manager's state](0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md)
|
||||
- **0208** — [The graphical session is one module per piece, on the mesh's seats](0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md)
|
||||
|
||||
### How it is built
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ code:
|
||||
- mesh-tools src/broker-amqp.ts (to be replaced)
|
||||
- mesh-catalog modules/nats (to be written)
|
||||
- mesh-sdk src (the protocol's NATS binding, step 3)
|
||||
updated: 2026-10-02
|
||||
- mesh-tools node-tools/internal/bus (a module's state, ADR 0201)
|
||||
updated: 2026-10-04
|
||||
decisions:
|
||||
- 02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md
|
||||
- 02-DECISIONS/0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md
|
||||
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||
@@ -48,6 +50,7 @@ mesh's own state lives, and where what a module may say is decided by what it de
|
||||
| **events** — a module says something happened | 1:many | delivered to every consumer that declared it; dead-lettered when it cannot be |
|
||||
| **tools** — a module or a person asks another's tool | request/reply | one answer, from one server, or a timeout |
|
||||
| **work to a role** — a module submits to a capability without knowing who provides it | job | exactly one holder does it; it queues while nobody does |
|
||||
| **state** — a module's current value of something, every machine reading it | key-value | the newest per key, kept until replaced or deleted; read whole by a machine that joins later |
|
||||
|
||||
The last two rows are the ones worth dwelling on, because they are not messaging in the sense of
|
||||
carrying bytes from A to B. **A role is addressable**, so a caller names the capability and never
|
||||
@@ -56,7 +59,8 @@ changing ([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)
|
||||
property of the mesh's architecture that happens to be expressed in subjects.
|
||||
|
||||
And more of the mesh lands here as it is built: conditions and observed state in key-value
|
||||
buckets that anything may watch, the server's own advisories becoming observations like any other
|
||||
buckets that anything may watch — the first of them a module's own declared state, *2026-10-04*
|
||||
([ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)) — the server's own advisories becoming observations like any other
|
||||
([research 017](../../01-RESEARCH/017-a-mesh-that-heals-itself/00-overview.md)), and a person's
|
||||
client speaking the bus directly rather than through a surface built over it (§7). None of that
|
||||
is a message being moved; all of it is the bus being the mesh's centre.
|
||||
@@ -84,8 +88,16 @@ mesh.seat.<seat>.event.<verb> a role's own event (JetStream: EVENT
|
||||
mesh.seat.<seat>.tool.<verb> a role's tool (core request/reply)
|
||||
mesh.ask.<node>.<command> the controller's command api (core request/reply)
|
||||
mesh.assignment.<node>.<module> an assignment's membership (JetStream: ASSIGNMENTS, last-per-subject)
|
||||
$KV.<module>_<name>.<key> a module's state (JetStream: a key-value bucket per declared name)
|
||||
```
|
||||
|
||||
**Added 2026-10-04** ([ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)):
|
||||
the last row is outside `mesh.` on purpose. A key-value bucket is NATS's own construct and lives
|
||||
under NATS's own prefix, which is what lets the server's key-value layer — direct reads, rollups,
|
||||
delete markers, watches — do the work instead of the mesh writing it again. The bucket is named for
|
||||
the module and the local name joined by an underscore, which neither may contain, so two modules can
|
||||
never derive one bucket.
|
||||
|
||||
**Revised 2026-10-01** ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)): the rows above
|
||||
for a module's and a seat's tools are the shapes the controller *issues*, not rules a runtime carries.
|
||||
Every assignment is published a membership — what it serves and where, in which queue, its seat verbs,
|
||||
@@ -155,6 +167,7 @@ Core NATS is at-most-once. Everything the mesh must not lose lives in a JetStrea
|
||||
| CONTROL | `mesh.control.>` except `alive` (a build's outcome moved to its seat, ADR 0121) | work queue, one consumer (the controller), explicit ack | the store-window guarantee ([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)): the controller `nak`s with a delay while its store is away and the message is redelivered; nothing is dropped |
|
||||
| NODES | `mesh.node.>` | last per subject | one declaration per node, always the newest |
|
||||
| EVENTS | `mesh.mod.*.event.>` and `mesh.seat.*.event.>` | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream). *2026-10-01:* a build's whole log is here too, as the build-machine seat's `log.<build id>` events ([ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)) — one subject per build, a week of retention, read back by `builds --log <id>` with a consumer that is gone when the reading is done |
|
||||
| `KV_<module>_<name>` | `$KV.<module>_<name>.>` | the newest value per key — as many past values as the owner declared — no age unless the owner declared one; a value at most 256 KiB, a bucket at most 64 MiB | a module's state (ADR 0201): one per name in a manifest's `state`, created from the catalogue on every raise, so it exists before its owner runs anywhere; kept when the module is unassigned, because what it holds is data |
|
||||
|
||||
Tool calls and heartbeats stay on core NATS: a lost heartbeat is the next heartbeat; a lost tool
|
||||
call is a timeout the caller already handles.
|
||||
@@ -234,6 +247,14 @@ expresses this exactly, per subject, and better than a vhost could:
|
||||
permissions for each consumed event's subject, its tool subjects, and that same inbox prefix.
|
||||
Nothing else. A module that tries to publish outside its emits is refused by the server, not by
|
||||
convention.
|
||||
- **A module's state** (ADR 0201), for whichever principal carries the module — today the machine's
|
||||
runtime, whose grant is the union of its modules': binding to the bucket, reading a key directly,
|
||||
and an ordered consumer for listing and watching, created and deleted on the bucket's own stream
|
||||
and nothing else's; and, for the owner's instances only, publishing under the bucket's own
|
||||
`$KV.<bucket>.>`. *Measured 2026-10-04 against a running server:* without the consumer-delete
|
||||
grant a watch cannot be stopped cleanly, and a write the server refuses reaches the writer as a
|
||||
timeout rather than a refusal — so the runtime refuses first, from the membership, and the grant
|
||||
is the second line.
|
||||
- **The controller's user** owns `mesh.control.>`, `mesh.node.>` and the streams, and may submit work
|
||||
to the seats the mesh's own flows use — a build, for one (ADR 0121).
|
||||
**A host's user** may publish its own `mesh.control.<node>.>` and subscribe its own
|
||||
|
||||
@@ -14,7 +14,7 @@ decisions:
|
||||
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
|
||||
- 02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md
|
||||
- 02-DECISIONS/0038-the-mesh-assigns-the-port.md
|
||||
- 02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md
|
||||
- 02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md
|
||||
---
|
||||
|
||||
# 27 — A module requires, the mesh resolves
|
||||
@@ -208,7 +208,7 @@ the placeholder allows: the definition says which values reach which requirement
|
||||
does. *How it is checked:* the unit tests named in issue 173, and the plan comparison that closed it.
|
||||
|
||||
*A provider says once what it derives for each consumer (2026-10-02,
|
||||
[ADR 0201](../../02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md),
|
||||
[ADR 0202](../../02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md),
|
||||
[issue 124](../../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md)):*
|
||||
where a provider **names the resource** it gives each consumer — a bucket, a database, a vhost — the
|
||||
name is derived per consumer, and a literal `serves` block could not carry it. A served value may
|
||||
@@ -221,7 +221,7 @@ its binding's served facts and as `${bound:<provision>:<key>}` in any file it wr
|
||||
as `derived` on that consumer's entry in its contributions file, so its provisioner is told the name
|
||||
rather than recomputing it. A consumer that writes the derived value into its own definition instead
|
||||
of asking for it is refused, naming the placeholder to use. *How it is checked:* the unit tests in
|
||||
ADR 0201's "how this is checked", each run against the unchanged controller first.
|
||||
ADR 0202's "how this is checked", each run against the unchanged controller first.
|
||||
|
||||
## How a definition reads what was resolved
|
||||
|
||||
|
||||
@@ -11,8 +11,10 @@ code:
|
||||
- mesh-host internal/apply/apply.go
|
||||
- mesh-tools src/main.ts
|
||||
- mesh-catalog modules/mesh-catalog
|
||||
updated: 2026-10-02
|
||||
- mesh-tools node-tools/internal/runtime (a module's state, ADR 0201)
|
||||
updated: 2026-10-04
|
||||
decisions:
|
||||
- 02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md
|
||||
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||
@@ -67,6 +69,8 @@ the catalogue, and the mesh would have hundreds of copies of a decision it made
|
||||
| `tools: status` | queue-group subscription on `mesh.mod.<module>.tool.status` |
|
||||
| seat `telegram-sender`, `accepts: send` | work-queue consumer on `mesh.seat.telegram-sender.accept.send` |
|
||||
| `uses: telegram-sender` | publish on that seat's `accept` subjects, and nothing else |
|
||||
| `state: servers` | a key-value bucket for the module, created by the controller; its instances write and read it |
|
||||
| `reads: billing.orders` | read and watch billing's `orders` bucket, and nothing else of it |
|
||||
|
||||
**Wildcards, and they are the mesh's rather than a bus's.** *Added 2026-09-27, from
|
||||
[issue 127](../../04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/00-report.md).* A
|
||||
@@ -221,7 +225,7 @@ service" versus "one worker per machine".
|
||||
| credential | sealed, per consumer | none | none | none | none |
|
||||
| reply | — | none | none, or an event later | a report | awaited |
|
||||
| retention | — | age and size | work queue, explicit ack | **last per subject** | none |
|
||||
| declared | `provides`/`requires` | `emits`/`consumes` | seat `accepts` / `uses` | the mesh's own | `serves` |
|
||||
| declared | `provides`/`requires` | `emits`/`consumes` | seat `accepts` / `uses` | the mesh's own; a module's `state` / `reads` | `serves` |
|
||||
|
||||
**Job** is the one [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) had no room
|
||||
for. Its table has events at 1:many and provisions at 1:1; a module submitting work to a service
|
||||
@@ -236,6 +240,31 @@ last-per-subject retention, and a node that has seen sequence *n* refuses *n−1
|
||||
That is the wire-level answer to
|
||||
[issue 107](../../04-ISSUES/107-a-declaration-carries-no-order/00-report.md).
|
||||
|
||||
**A module declares state too.** *Added 2026-10-04,
|
||||
[ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md).*
|
||||
State was the mesh's alone, and modules had the same need with nowhere to put it: an MCP server
|
||||
registered for every machine, sent as an event, never reached a machine assigned afterwards — its
|
||||
consumer did not exist yet when the event passed — and a licence binding sent as events replays a
|
||||
week of rotations where only the latest matters. So a module names the state it **owns** with
|
||||
`state`, and another module's it **reads** with `reads: <module>.<name>`. Each is a key-value bucket
|
||||
the controller creates from the catalogue, mesh-wide, existing from registration so a reader can
|
||||
watch before the owner runs anywhere ([design 25](25-the-bus-on-nats.md) §3). Every instance of the
|
||||
owner writes; a reader reads and watches. A key may name a machine by the module's own convention;
|
||||
the mesh keeps one bucket per name, not one per machine, because "every server, for every machine"
|
||||
is then one list rather than a walk.
|
||||
|
||||
What a module sees is what it named. Its assignment's membership lists its buckets by those names,
|
||||
with whether it may write, and the runtime answers `get`, `put`, `delete`, `keys` and `watch` for
|
||||
them on the bundle's channel — refusing, with the reason, a name it was not issued or a write to a
|
||||
bucket it only reads. A watch hands the current values first, then every change: a bundle that starts
|
||||
late, or starts again, has the whole of the state before it has any of the news.
|
||||
|
||||
The owner says how many past values a key keeps and how long a value lives, as a seat says how long
|
||||
its backlog survives (§3); the mesh caps a value's size and a bucket's. **A bucket outlives its
|
||||
module's assignment** — what a module stored is data
|
||||
([ADR 0030](../../02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md)) — and one whose
|
||||
declaration is gone is reported, never removed by the mesh.
|
||||
|
||||
## 5. Seats
|
||||
|
||||
A module declares a seat with its protocol, and the mesh enforces one holder at its scope
|
||||
@@ -454,6 +483,14 @@ sealing key leaks, that stream is an archive rather than a moment. So:
|
||||
existing discipline — *fetched from it, not carried* — applied to the one payload where carrying
|
||||
it is worst.
|
||||
|
||||
**A key-value bucket is a stream, so the same holds for it.** *Added 2026-10-04,
|
||||
[ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md).*
|
||||
No secret is put in a module's state, sealed or not: state is exactly what a machine joining a year
|
||||
later reads in full. A value that needs a secret names it, and the secret travels on request/reply.
|
||||
Sealed values are plain text to anything inspecting them, so this is checked only partly — the
|
||||
runtime refuses a value carrying a field whose name says it is a credential, which catches the
|
||||
ordinary mistake and not a determined one.
|
||||
|
||||
**The bootstrap, which is circular and has a precedent.** The vault makes every secret
|
||||
([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)), including the bus's own
|
||||
passwords. The vault is a module, and a module needs a bus account, whose password the vault
|
||||
@@ -523,3 +560,9 @@ billing existing under that name.
|
||||
on, and exactly those two are rebuilt.
|
||||
- **A stale declaration is refused.** A bed: replay sequence *n−1* after *n*, and the node refuses
|
||||
it rather than applying it.
|
||||
- **A module reaches only the state it declared.** The composer's test: an owner's runtime may write
|
||||
its buckets, a reader's may only read, and nothing else is granted; the runtime's test over a real
|
||||
bus: a name not issued and a reader's write are refused with the reason.
|
||||
- **State is current at once.** The runtime's test over a real bus: a watch hands the current values
|
||||
without deletions, then an end-of-current marker, then changes. Live: a machine assigned after a put
|
||||
reads it at start.
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-10-02
|
||||
updated: 2026-10-04
|
||||
decisions:
|
||||
- 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md
|
||||
- 02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
|
||||
- 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
|
||||
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
|
||||
@@ -54,8 +55,10 @@ instruction file and the manager's tools:
|
||||
| the console's entry in the agent's user-scope state | the managed settings' tool-server key, from the console's provision (§4) |
|
||||
|
||||
**The home.** Under [ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
||||
every path under `~/.claude` is *found*, with one exception: the agent's credentials file, which the
|
||||
module's own code writes for a subscription licence (§5). The person's memory, history, projects, local
|
||||
the module owns the directory `~/.claude` — that it exists, that the operator owns it, its mode,
|
||||
`0700` — and declares it, so the mesh refuses a second module owning it. Of what is inside, it owns only
|
||||
the agent's credentials file, which its own code writes for a subscription licence (§5); every other path
|
||||
is *found*. The person's memory, history, projects, local
|
||||
settings, their own rules, skills and tool servers are never read or written by the mesh. **The six
|
||||
predecessor files are the operator's to remove, once, on each workstation**; the module's documentation
|
||||
lists them, and until they go the agent reads stale instructions beside the mesh's.
|
||||
@@ -63,9 +66,12 @@ lists them, and until they go the agent reads stale instructions beside the mesh
|
||||
## 2. What the module declares and what its code writes
|
||||
|
||||
**Declared, applied by the host:** the agent's package (§7); the module's state directory; a facts file
|
||||
in that directory carrying the node's name, the operator account, the console's endpoint, the module's
|
||||
settings; the bus, the console's provision, and that it uses the `anthropic-licence-manager` seat.
|
||||
Nothing under the home, nothing under `/etc`.
|
||||
in that directory carrying the node's name and the console's endpoint, and a settings file carrying the
|
||||
role and the extra tool servers, merged from the module's settings layers — the bundle is told the two
|
||||
files' paths, because a bundle's words are paths and constants only (ADR 0192); the bus, the console's provision, and that it uses the `anthropic-licence-manager` seat.
|
||||
Two directories, declared so the ownership check sees them: the agent's managed directory under
|
||||
`/etc`, root's, and `~/.claude` under the operator's home, the operator's. No *file* resource under
|
||||
either: what is in them is written by the module's code (§2 below) or is the person's.
|
||||
|
||||
**Written by the module's code**, from the facts file and the manager's hand-over, whenever either
|
||||
changes:
|
||||
@@ -111,17 +117,20 @@ the playbooks in the record.
|
||||
|
||||
## 4. The console
|
||||
|
||||
The module tells the agent where the console is, and the port is the console's to say. **The console
|
||||
provides a node-scoped provision** — its MCP endpoint on loopback — serving the port the machine gave
|
||||
it, and the module requires it. A requirement names what the consumer is coupled to
|
||||
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)); co-location
|
||||
resolves it; a machine without the console refuses the module by name. [To-be 34](34-the-console.md) is
|
||||
amended in the same change; issue 192 (open) found the gap.
|
||||
> **Revised 2026-10-03, building it.** The vendor's managed-settings key for tool servers refuses any
|
||||
> URL that is not `https://`, including one on loopback, so it cannot carry the console. The module
|
||||
> owns the vendor's **exclusive** managed tool-server file instead (operator's choice): the console as
|
||||
> `mesh`, over HTTP on loopback, and every server in the module's `mcp_servers` setting — and no other.
|
||||
> A server added by hand, a project's own file and a plugin's servers stop loading; claude.ai's
|
||||
> connectors are kept by a managed setting. A person's own servers move into the setting, for the mesh
|
||||
> or for one node. Stdio straight to the bus, through the runtime's own client, was weighed and left
|
||||
> for later: it needs a verb the delivered runtime does not have, and a session holding its own bus
|
||||
> connection breaks on a credential rotation.
|
||||
|
||||
**Other tool servers** a person wants on every machine, or on one, are a declared setting of this module
|
||||
— mesh layer or node layer — rendered into the same managed key. A module tool, `mcp_configure`,
|
||||
validates a server and sets the setting through the controller's settings verb, so the list stays
|
||||
declared state. The agent's own HTTP-only constraint for managed servers applies; a person's local
|
||||
— mesh layer or node layer — rendered into the same managed file. The person sets them with the
|
||||
controller's `settings` verb on this module, so the list stays declared state; the list is the operator's
|
||||
choice, set where every setting is set. The agent's own HTTP-only constraint for managed servers applies; a person's local
|
||||
command-based servers stay their own, in their own file.
|
||||
|
||||
**The entry's name is `mesh`.** The hand-made entry both workstations carry today is named after this
|
||||
@@ -131,36 +140,42 @@ it is the person's to remove, and until then the agent sees the mesh's tools twi
|
||||
## 5. The licence: the consumer side
|
||||
|
||||
[ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
|
||||
decides it; to-be 39 is the manager's half. This module:
|
||||
decides it and [ADR 0206](../../02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md) says how it moves; to-be 39 is the manager's half. This module:
|
||||
|
||||
- **makes a keypair** in its state the first time it runs and registers the public half with the seat;
|
||||
- **serves `apply`**: the manager's hand-over, a token sealed to the module's key, with the licence's
|
||||
name and kind. A rotation of the same licence is applied only if newer within one lineage; a switch is
|
||||
applied regardless, because across licences the expiries are unrelated. The answer says applied or
|
||||
refused and why, and never echoes a token;
|
||||
- **pulls** at start and when its token nears expiry, by the seat's `current` verb, and keeps the last
|
||||
token when the manager does not answer, saying so;
|
||||
- **writes** for a subscription licence the credentials file as the operator, access-token-only; for the
|
||||
API-key licence sets the key-helper in the managed settings to a small program that prints the key
|
||||
from the module's state, so no file under the home is touched;
|
||||
- **offers a login to the manager**: when the credentials file changes by a person's login, it reads the
|
||||
account's identity from the agent's state file and offers the grant to the seat, sealed to the manager's
|
||||
key, for adoption; the manager decides;
|
||||
- **serves `licence_status`**: which licence and kind this node holds, when the token expires, whether
|
||||
- **makes a keypair** in its state the first time it runs, and sends the public half with every request
|
||||
that is answered sealed;
|
||||
- **reports what the node holds**, as its own `holdings` state, one key for this node: the account's
|
||||
identity read from the agent's state file, the kind, the refresh token's fingerprint and whether one is
|
||||
present, the access token's fingerprint and expiry, the licence and generation it last applied, when the
|
||||
credentials file last changed. Written at start — a node already logged in reports at once — and on every
|
||||
change of the file. Never a token: the runtime refuses one anyway;
|
||||
- **hands over a grant only when asked**: `claude_code_grant` answers the manager, which gives its public
|
||||
key, with the full grant in the credentials file sealed to that key — the one time a refresh token
|
||||
leaves the node, for the manager to adopt by refreshing it;
|
||||
- **watches the manager's `bindings` state** for this node, and when the generation is newer than the one
|
||||
it applied, asks the seat's `current` verb for the token, sealed to its own key. A rotation of the same
|
||||
licence is applied only if newer within one lineage; a switch is applied regardless;
|
||||
- **writes** for a subscription licence the credentials file as the operator, **access-token-only** — so
|
||||
the agent here never refreshes, and a refresh token appearing later is a person's login, reported like
|
||||
any change; for the API-key licence sets the key-helper in the managed settings to a small program that
|
||||
prints the key from the module's state, so no file under the home is touched;
|
||||
- **serves `claude_code_status`**: which licence and kind this node holds, when the token expires, whether
|
||||
the file matches what was handed over — by fingerprint, never by value.
|
||||
|
||||
Switching is the seat's `switch` verb, asked through the console; this module only applies what it is
|
||||
handed.
|
||||
Switching is the seat's `switch` verb, asked through the console; this module only applies what the
|
||||
state says it should hold. *2026-10-04:* this replaces the manager's visits of 2026-10-03 (ADR 0183's dated
|
||||
note): the node reports, the manager asks for a secret only when a report shows one it does not hold, and
|
||||
a token is fetched by request when the state says it changed.
|
||||
|
||||
## 6. Scope, settings and the order of assignment
|
||||
|
||||
**Every node with an operator account** ([ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)).
|
||||
None has one today; the operator states them first. **Per node:** the role. **Per mesh or per node:**
|
||||
All four nodes carry one since 2026-10-03. **Per node:** the role. **Per mesh or per node:**
|
||||
extra tool servers. **Prerequisite:** the manager holds its seat and has adopted the licences.
|
||||
|
||||
**Order:** the manager assigned and a refresh observed; the console's provision in the catalogue; this
|
||||
module on one workstation; the six predecessor files and the hand-made console entry removed there; a
|
||||
new session read to confirm it sees the mesh's instruction file, the console's tools under `mesh`, and
|
||||
new session read to confirm it sees the mesh's instruction file, the console's five tools under `mesh`, and
|
||||
its licence; then the rest.
|
||||
|
||||
## 7. The package
|
||||
@@ -178,13 +193,13 @@ installer is rejected: it puts a self-updating binary under the person's home, i
|
||||
|
||||
| Check | Defends |
|
||||
|---|---|
|
||||
| the module's definition names no node, path or login, declares nothing under a home or `/etc`, and no file resource carries a secret | ADR 0112, ADR 0155, ADR 0183 |
|
||||
| the module's definition names no node, path or login, declares no file under a home or `/etc` (only the two directories), and no file resource carries a secret | ADR 0112, ADR 0155, ADR 0183 |
|
||||
| on a lab machine with an account and a seeded home holding a person's rule file and the predecessor's leftovers: after assign, the managed directory holds the mesh's files, the home is byte-identical except the credentials file, which is owned by the operator and names no refresh token; after unassign, the managed directory's files are gone and the home is untouched | ADR 0182, the host's agnosticism |
|
||||
| on a lab machine with no account, the assignment is refused naming the fact | ADR 0181 |
|
||||
| a switch asked of the seat through the console changes the licence and the token on the node; no tool answer and no log line holds a token | ADR 0183 |
|
||||
| the API-key binding writes nothing under the home and the agent authenticates through the helper | ADR 0183 |
|
||||
| the console's provision resolves by co-location; a machine without the console refuses the module by name | ADR 0027, ADR 0152 |
|
||||
| a new session on the assigned workstation lists the console's tools under `mesh` and answers "which node am I" from the instruction file | the exit of the build |
|
||||
| a new session on the assigned workstation lists the console's five tools under `mesh` ([ADR 0195](../../02-DECISIONS/0195-the-meshs-tools-are-found-by-address-not-announced-whole.md)) and answers "which node am I" from the instruction file | the exit of the build |
|
||||
|
||||
## What this does not settle
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-host, mesh-controller, mesh-tools, mesh-catalog]
|
||||
updated: 2026-10-02
|
||||
updated: 2026-10-04
|
||||
decisions:
|
||||
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
|
||||
- 02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md
|
||||
@@ -33,12 +33,16 @@ This is the design [to-be 29](29-a-node-has-operator-accounts.md) §2 called *a
|
||||
Worked on the first one, a shell. The `zsh` module declares:
|
||||
|
||||
- a **package**, `zsh`;
|
||||
- **files under the home**, owned by the account: the shell's rc file with the module's default
|
||||
configuration, carrying a kept region for the operator's own lines, and `${setting:…}`
|
||||
placeholders for the few values a node varies; the account and its home are machine facts the
|
||||
controller resolves ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md),
|
||||
to-be 29 §2);
|
||||
- a **seat declaration**, `login-shell`, node-scoped, with its one verb; and a **claim** on it;
|
||||
- **files under the home**, owned by the account: the mesh's block at the start of the shell's rc
|
||||
file with the module's default configuration and the slots other modules' code lands in, the
|
||||
operator's own lines kept after it, and `${setting:…}` placeholders for the few values a node
|
||||
varies; the account and its home are machine facts the controller resolves
|
||||
([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md),
|
||||
to-be 29 §2). Its environment is a contribution to the environment module, not lines of its own
|
||||
([ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md),
|
||||
[ADR 0204](../../02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md),
|
||||
[to-be 41](41-the-shell-and-the-accounts-environment.md));
|
||||
- a **claim** on the mesh's node-scoped seat `node-login-shell`, with its one verb;
|
||||
- a **`user` shape** naming the shell, applied only where the module holds the seat;
|
||||
- a **tools bundle**, the artifact kind for interpreted code, with `execute` and the module's own
|
||||
`show-config`.
|
||||
@@ -80,7 +84,8 @@ root escalates itself.
|
||||
|
||||
## 4. The seats of the environment
|
||||
|
||||
Decided now: **`login-shell`** (module-declared; zsh, fish, bash; verb `execute`) and
|
||||
Decided now: **`node-login-shell`** (the mesh's own, ADR 0204; zsh, fish, bash; verb `execute`),
|
||||
**`node-environment`** (the mesh's own, ADR 0203; the environment module; no verbs) and
|
||||
**`node-service-manager`** (the mesh's own; systemd; verbs over units in both scopes). The rest
|
||||
are candidates from [research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md),
|
||||
one record each when its first holder is written: display server, display session, terminal
|
||||
|
||||
@@ -15,6 +15,7 @@ decisions:
|
||||
- 02-DECISIONS/0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md
|
||||
- 02-DECISIONS/0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md
|
||||
- 02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md
|
||||
- 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md
|
||||
---
|
||||
|
||||
# 38. Building the operator's machine
|
||||
@@ -330,6 +331,13 @@ tool of each.
|
||||
|
||||
## WP5 — The shell, on a server first
|
||||
|
||||
*Replaced on 2026-10-04 by [to-be 41](41-the-shell-and-the-accounts-environment.md).* A review before
|
||||
assigning found that the shell module would duplicate every machine's existing startup file, drop
|
||||
lines from it, leave the prompt uninstalled, and could not be unassigned
|
||||
([issue 228](../../04-ISSUES/228-a-login-the-mesh-set-is-never-given-back/00-report.md)). The shell,
|
||||
its environment, the modules that plug into it, and the host's fix are built and proven there. What
|
||||
follows is the original plan, kept for the record.
|
||||
|
||||
*mesh-catalog #224, already written. Half a day to assign and prove.*
|
||||
|
||||
**Order.** Assign `zsh` to one server; push; `login-shell.execute@<server> command="uptime"`
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-10-02
|
||||
updated: 2026-10-04
|
||||
decisions:
|
||||
- 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md
|
||||
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
|
||||
- 02-DECISIONS/0024-model-access-is-a-provision.md
|
||||
- 02-DECISIONS/0050-model-access-is-vendor-agnostic.md
|
||||
@@ -74,22 +75,22 @@ Carried from the predecessor, where each rule was earned by an incident:
|
||||
|
||||
## 4. Handing a token to a node
|
||||
|
||||
Every node that runs the agent module registers that module's public key with the seat when it first
|
||||
runs. From then on:
|
||||
*Amended 2026-10-04 by [ADR 0206](../../02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md)*, replacing the manager's visits: **what each consumer
|
||||
should hold is the manager's state, and the token is fetched when it changes.**
|
||||
|
||||
- **On rotation**, the manager calls `claude-code.apply@<node>` on every node bound to the rotated
|
||||
licence, with the new token sealed to that node's module key. The module answers *applied*, or
|
||||
*refused* and why, and the manager records it.
|
||||
- **On a switch**, the same call with the other licence's token, and the binding is the authority: the
|
||||
module applies a bind without comparing expiries, because across two licences the numbers are
|
||||
unrelated.
|
||||
- **On a pull** — the module starting, or finding its token near expiry — the module calls the seat's
|
||||
`current` verb for its binding and is answered sealed the same way.
|
||||
- **Never as an event.** What the manager emits names the licence and the outcome and carries no token.
|
||||
- **The manager keeps a `bindings` state**, one key per consumer: the licence, its kind, and a
|
||||
**generation** that increases with every rotation and every switch. Nothing in it is secret.
|
||||
- **The agent module on each node watches its own key.** When the generation is newer than the one it
|
||||
applied, it asks the seat's `current` verb, sending its public key, and is answered with the token
|
||||
sealed to it — request/reply, never an event. A node that was away reads its key when it is back and
|
||||
asks once; a manager that is down leaves every node on its last token, which lives hours.
|
||||
- **On a switch** the agent applies the new licence's token without comparing expiries, because across
|
||||
two licences the numbers are unrelated; within one licence it applies only a newer grant.
|
||||
- **No event announces a rotation or a switch.** What they announced is the state itself, and a node
|
||||
needs the latest, not the history. What the manager still emits names an outcome and carries no token.
|
||||
|
||||
A node whose module has not registered a key cannot be handed a token, and the manager says so by name
|
||||
rather than falling silent. A node whose module refuses — a wrong identity, a stale grant within one
|
||||
lineage — is recorded as drift and reported.
|
||||
A consumer that never asks is visible: its own report (§6) names the licence and generation it holds,
|
||||
and a node behind its binding is drift the manager reports.
|
||||
|
||||
## 5. Who gets which licence
|
||||
|
||||
@@ -116,27 +117,46 @@ already keeps.
|
||||
|
||||
## 6. Adopting a grant
|
||||
|
||||
A licence enters the mesh one of two ways, and the token never passes through a prompt, a terminal or an
|
||||
argument:
|
||||
*Amended 2026-10-04 by [ADR 0206](../../02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md)*: a licence is an account, learned from what the nodes
|
||||
report, and adopted by refreshing it.
|
||||
|
||||
- **From a node's login.** A person logs in on a node, as they always have. The agent module there reads
|
||||
the account's identity from the agent's own state file, and offers the full grant to the seat sealed
|
||||
to the manager's key. The manager adopts it into the licence the node is bound to **only if the
|
||||
identity matches** that licence's recorded account; a licence not yet identified is identified by its
|
||||
first adoption; a mismatch is refused and notified, because the predecessor once filed one account's
|
||||
grant into another's row this way.
|
||||
- **Every node reports what it holds**, as the agent module's `holdings` state, one key per node: the
|
||||
account's identity read from the agent's own state file, the kind, the refresh token's fingerprint and
|
||||
whether one is present, the access token's fingerprint and expiry, the licence and generation it was
|
||||
last handed, when the credentials file last changed. Written when the module starts — a node already
|
||||
logged in reports at once — and on every change. Never a token.
|
||||
- **The manager reads every report at start and watches them.** A report with a refresh token whose
|
||||
fingerprint the manager does not hold is a candidate: a new licence for an account it has none for, a
|
||||
login made since for one it has. A manager launched for the first time holds no licence and takes every
|
||||
report as a candidate.
|
||||
- **The secret is asked for, never published.** For a candidate the manager calls that node's agent
|
||||
module, giving its own public key, and is answered with the grant sealed to it.
|
||||
- **Adopting is refreshing.** The manager exchanges the candidate's refresh token under its lease for
|
||||
that account; success makes the returned grant the licence's and the manager its only rotation source;
|
||||
failure records the candidate dead and adopts nothing. Candidates for one account are tried newest login
|
||||
first, and the first that refreshes ends the search — the others are never exchanged.
|
||||
- **The latest login wins.** A bound node is handed an access token only and its file holds no refresh
|
||||
token, so a refresh token appearing there later is a person's login; its report makes it a candidate,
|
||||
and if it refreshes it replaces the licence's grant.
|
||||
- **A first binding follows the login**: a node with no binding whose report names the adopted account
|
||||
is bound to it. Every later change is `bind`, `switch` or `release`.
|
||||
- **The identity guard** files a grant under the identity the node read; where the vendor's refresh
|
||||
answer names the account too, a mismatch is refused and notified. Which source decided is audited.
|
||||
- **An API key** is delivered to the manager by the operator through the seat's `adopt` verb from a file
|
||||
on the manager's node, never as an argument.
|
||||
|
||||
## 7. What it emits and serves
|
||||
|
||||
**Events**, no secret in any: `licence.rotated`, `licence.switched`, `licence.adopted`,
|
||||
`licence.failing`, `licence.refused`, `usage.read` — the audit logger records them all.
|
||||
**Events**, no secret in any: `licence.adopted`, `licence.failing`, `licence.refused`, `usage.read` —
|
||||
the audit logger records them all. *2026-10-04 (ADR 0206):* `licence.rotated` and `licence.switched` are
|
||||
gone; a rotation or a switch is a new generation in the `bindings` state.
|
||||
|
||||
**State**: `bindings`, which it keeps; the agent module's `holdings`, which it reads.
|
||||
|
||||
**The seat's verbs**, the contract every future holder must serve: `licences` (each with kind,
|
||||
identity, expiry, failures, who is bound), `bindings`, `bind`, `switch`, `release`, `refresh` (now, one
|
||||
or all), `usage` (current and history), `adopt`, `register` (a node's module key), `current` (a
|
||||
consumer's token, sealed, asked by the consumer's module).
|
||||
or all), `usage` (current and history), `adopt`, and `current` (a consumer's token, sealed to the key the
|
||||
consumer sends — ADR 0206). The manager asks a node for a candidate grant by the agent module's own tool.
|
||||
|
||||
## 8. Settings
|
||||
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-10-04
|
||||
decisions:
|
||||
- 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md
|
||||
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
|
||||
- 02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
|
||||
- 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
|
||||
- 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
|
||||
- 02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md
|
||||
- 02-DECISIONS/0192-a-tools-bundle-declares-what-it-is-given-and-the-runtime-hands-it-to-that-bundle-alone.md
|
||||
- 02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md
|
||||
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
|
||||
---
|
||||
|
||||
# 40. Building the operator's agent and its licence manager
|
||||
|
||||
**The work of [design 36](36-the-operators-agent-on-a-machine.md) and [design 39](39-the-anthropic-licence-manager.md),
|
||||
broken into packages that each end at something a person can see run, in the order their
|
||||
dependencies allow.** The two designs are the authority on *what* is built; this document holds the
|
||||
packages, their order, their sizes and their proofs, and is wrong the moment it disagrees with them.
|
||||
It is the shape [design 38](38-building-the-operators-machine.md) gives the operator's machine.
|
||||
|
||||
*Revised 2026-10-03, after design 38's WP1–WP4b ran:* the node's tool runtime is live on all four
|
||||
machines, tools are bundles it serves and each is given only the words its artifact declares, every
|
||||
bundle is a child the runtime launches over stdio and is the bus for
|
||||
([ADR 0193](../../02-DECISIONS/0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md),
|
||||
[ADR 0198](../../02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)),
|
||||
and the console offers five tools over addresses
|
||||
([ADR 0195](../../02-DECISIONS/0195-the-meshs-tools-are-found-by-address-not-announced-whole.md)). What changed
|
||||
in this plan: the wait on design 38's WP3 is over; the manager starts every exchange, by the operator's
|
||||
direction (ADR 0183's dated note); and the manager's daemon is a long-running bundle the runtime launches,
|
||||
which ADR 0198 decided the same day — nothing in this plan waits on another record.
|
||||
|
||||
## How this is built, and where it is run
|
||||
|
||||
**On the live mesh, by the operator's decision** ([ADR 0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md)).
|
||||
Every package is written with unit tests, committed on one branch per repository
|
||||
([playbook 07](../../00-META/process/07-feature-branches.md)), and proven on the machines: one
|
||||
workstation first for the agent, the control node first for the manager, then the rest. A broken agent
|
||||
module leaves a workstation's agent without the mesh's instructions or with a stale token until the next
|
||||
push; the person's own files under the home are out of the failure's reach, by
|
||||
[ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md).
|
||||
|
||||
Each package names what proves it. A package that cannot name its proof is divided until it can.
|
||||
|
||||
## What exists already, measured
|
||||
|
||||
Measured 2026-10-03 on the four machines and in the repositories.
|
||||
|
||||
| Piece | Today | Becomes |
|
||||
|---|---|---|
|
||||
| the node's tool runtime | live on all four, a host process; **runs as the operator account**; listens for the console on loopback at a port its own code fixes; launches or imports every assigned module's tools bundle and hands each its declared words | serves the agent module's tools; gains one provision for its endpoint (WP1) |
|
||||
| the operator account | **stated on all four** — the runtime runs as it | read by the agent module from the runtime's own words |
|
||||
| escalation | passwordless `sudo` for the operator account on all four — a fact about the machines, checked by nobody | how the agent module writes its managed directory under `/etc` |
|
||||
| the agent itself | installed on all four, at four different versions, all above the one the managed tool-server key needs | declared as the module's package |
|
||||
| a bundle's words | paths and constants written with `${dir:…}` and `${port:…}` only; a fact the mesh knows reaches a bundle as a file whose path is a word | the agent module's facts file and settings file |
|
||||
| a bundle calling a tool | `mesh/ask` through the runtime that launched it (ADR 0198); no bundle holds a bus credential | how the manager visits every node |
|
||||
| a module's own long-running code | a bundle the runtime launches and restarts (ADR 0198); the runtime's subscription and grants built, the modules moving in design 38 WP4c's waves | the manager's daemon (WP3, WP4) |
|
||||
| the vendor's refresh, the sealed box, the grant file | `anthropic-manager` in the catalogue, built on the controller placement ADR 0183 moved away from; assigned to nothing | its client ported into the manager; the module retired (WP6) |
|
||||
| the credentials write, the strip, the identity read | `anthropic-consumer` in the catalogue; tested; assigned to nothing | ported into the agent module with its tests; the module retired (WP6) |
|
||||
| the predecessor's manager and consumer | the lease per licence, the expiry floor, the lineage comparison, the identity guard, three touchpoints, cooldowns | ported as logic with its tests |
|
||||
|
||||
## The order the work allows
|
||||
|
||||
```
|
||||
WP0 the operator names the licences and each node's role (the live mesh) ── an hour
|
||||
WP1 the runtime provides its endpoint (mesh-tools) ── small
|
||||
WP2 the agent module (mesh-catalog) ──┐ WP2 needs WP1;
|
||||
WP3 the manager's code, built and tested (mesh-catalog) ──┘ WP3 is independent
|
||||
│
|
||||
WP2 live: one workstation, configuration only — no licence yet ── the first live proof
|
||||
│
|
||||
WP4 the manager live on the control node ── its daemon a long-running bundle (ADR 0198)
|
||||
WP5 the licence end to end on one workstation
|
||||
WP6 the rest of the nodes, and the predecessor's remains
|
||||
```
|
||||
|
||||
## WP0 — The operator names the licences and each node's role
|
||||
|
||||
*The live mesh. An hour, and it is the operator's.* The accounts are stated already. What remains: the
|
||||
names of the two subscription licences and the API key; each node's role, as the agent module's setting
|
||||
on the node layer once the module is registered.
|
||||
|
||||
**Proof.** The module's settings list a role for every node; the licences have names.
|
||||
|
||||
## WP1 — The runtime provides its endpoint
|
||||
|
||||
*mesh-tools. An hour.*
|
||||
|
||||
**What changes.** The `node-tools` manifest provides a node-scoped provision, `mcp-endpoint`, serving
|
||||
the port its code listens on, the way the local model server serves its API
|
||||
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)). Co-location
|
||||
resolves it. The runtime's port stays what its code fixes; assigning it is
|
||||
[issue 192](../../04-ISSUES/192-the-meshs-tools-reach-a-person-only-by-a-registration-made-by-hand/00-report.md)'s
|
||||
second question and not this package's.
|
||||
|
||||
**Proof.** The plan for a workstation carrying a consumer of `mcp-endpoint` shows it bound to the
|
||||
runtime's port; the controller's tests and the catalogue's checks pass.
|
||||
|
||||
## WP2 — The agent module
|
||||
|
||||
*mesh-catalog. A day and a half.*
|
||||
|
||||
**What is written**, as design 36 says:
|
||||
|
||||
1. **The manifest.** The agent's package. A state directory. A facts file in it, rendered by the mesh:
|
||||
the node's name and the console's address from `mcp-endpoint`. A settings file in it, merged from
|
||||
the module's settings layers: the node's role and the extra tool servers. A tools bundle whose
|
||||
words name the two files, the state directory and nothing else. The two directories it owns declared — the agent's managed
|
||||
directory and `~/.claude` — and **no file resource under either.**
|
||||
2. **The renderer**, run whenever the runtime collects the module's tools: from the two files, the
|
||||
managed settings file (the tool servers under the entry `mesh`, the attribution trailers, the
|
||||
key-helper for an API-key binding) and the managed instruction file, written under the agent's
|
||||
managed directory through the account's escalation, only when their content changed.
|
||||
3. **The keypair**, made once in the state directory; X25519 and an authenticated cipher from the
|
||||
language's own library, so the bundle carries no dependency.
|
||||
4. **The tools and the state** (*2026-10-04, ADR 0206*): `claude_code_status` (what is rendered, what
|
||||
licence is held, when its token expires, fingerprints only); `claude_code_render` (render now);
|
||||
`claude_code_grant` (the full grant in the credentials file, sealed to the key the manager gives).
|
||||
The `holdings` state, written at start and on every change of the credentials file; a watch of the
|
||||
manager's `bindings` key for this node, which asks the seat's `current` on a newer generation and
|
||||
applies the sealed answer — only if newer within one lineage unless it is a switch; the credentials
|
||||
write as the operator, access-token-only, atomic; the key-helper program for an API key.
|
||||
5. **The documentation**: the six predecessor files and the hand-made console entry a person removes.
|
||||
|
||||
**Proof, before anything runs live.** Unit tests: the renderer writes the mesh's keys and nothing else;
|
||||
it writes nothing when nothing changed; the credentials write strips a refresh token and is atomic; the
|
||||
lineage cases from the predecessor; a sealed hand-over opens only with the module's key; no tool's answer
|
||||
contains a token. The catalogue's checks pass.
|
||||
|
||||
**Proof, live, on one workstation, configuration only.** Assign the module; set the node's role; push.
|
||||
The agent's managed directory holds the two files; everything under the person's agent directory is
|
||||
byte-identical to before; a new session lists the console's five tools under `mesh` and answers *which node am
|
||||
I* from the managed instruction file. `claude_code_status` answers through the console. No licence is
|
||||
touched: the module writes the credentials file only when it is handed a token.
|
||||
|
||||
## WP3 — The manager's code, built and tested
|
||||
|
||||
*mesh-catalog. Two to three days.*
|
||||
|
||||
**What is written**, as design 39 says: the manifest (the seat and its verbs, a database, a `secret`
|
||||
for the key the grants are encrypted with, a tools bundle and a long-running bundle for the daemon, both launched by the runtime, settings
|
||||
with defaults); the store's migrations; the refresh with its plan, lease, floor and cadence as pure
|
||||
functions; the vendor client from `anthropic-manager`; adoption from a file and from a node's waiting
|
||||
login with the identity guard; usage and its threshold; the seat's verbs. *2026-10-04 (ADR 0206):* in place
|
||||
of the visit, the watch of every node's `holdings`, adoption of a candidate by refreshing it (newest login
|
||||
first, once per account), the `bindings` state with a generation per consumer, and `current`.
|
||||
|
||||
**Proof, before anything runs live.** Unit tests: two refresh runs started together rotate one grant
|
||||
once; a mismatching identity is refused; a worker bound to a dead licence is refused and never lent
|
||||
another; nothing the daemon emits carries a token; a hand-over sealed for one node opens with no other
|
||||
node's key.
|
||||
|
||||
## WP4 — The manager live on the control node
|
||||
|
||||
*The live mesh. Half a day.* The daemon is a long-running bundle the control node's runtime launches
|
||||
(ADR 0198); it calls each node's agent module by `mesh/ask`. If the runtime's half of ADR 0198 is not
|
||||
yet live on the control node when this package starts, this package waits for it: no tool container, no
|
||||
credential copied by hand.
|
||||
|
||||
**Order.** Assign the manager on the control node; push. It reads every node's `holdings` and adopts each
|
||||
account the nodes are logged in to, by refreshing the newest login's grant (ADR 0206); each node with no
|
||||
binding is bound to the account it reported. Adopt the API key from a file there. A second subscription
|
||||
account enters by a login on a workstation carrying the agent module.
|
||||
|
||||
**Proof.** Through the console: `anthropic-licence-manager.licences` lists three licences with identity
|
||||
and expiry; within the cadence the audit shows a rotation and a later expiry; a forced `refresh` is
|
||||
logged with the vendor's answer.
|
||||
|
||||
## WP5 — The licence end to end on one workstation
|
||||
|
||||
*The live mesh. Half a day. The proof of the whole.*
|
||||
|
||||
**Order.** Record the checksums under the person's agent directory. Bind the workstation to a
|
||||
subscription licence. Remove the six predecessor files and the hand-made console entry. Start a session.
|
||||
|
||||
**Proof.** Everything under the person's agent directory is byte-identical but the credentials file,
|
||||
which is owned by the operator, readable by nobody else, and names no refresh token. A session makes a
|
||||
model request. `switch` to the second subscription licence changes the token on the workstation within a
|
||||
minute, and neither the verb's answer nor either module's log holds a token. Switched to the API key, the
|
||||
credentials file is left as it was and the agent authenticates through the key-helper. Switched back.
|
||||
|
||||
## WP6 — The rest of the nodes, and the predecessor's remains
|
||||
|
||||
*The live mesh and mesh-catalog. One day.* The module on every node, the predecessor's files removed on
|
||||
the second workstation; a login under a licence's account collected and adopted, and one under the wrong
|
||||
account refused and notified; `anthropic-manager` and `anthropic-consumer` retired from the catalogue;
|
||||
designs 36 and 39 set to `implemented` with the as-is written
|
||||
([playbook 02](../../00-META/process/02-graduation.md)).
|
||||
|
||||
**Proof.** `claude_code_status` answers on every node; the refusal's notification arrived; the catalogue
|
||||
has no module built on the old placement.
|
||||
|
||||
## What is deliberately not here
|
||||
|
||||
- **The package repository seat** for a distribution that does not carry the agent's package (design 36
|
||||
§7). The four machines have the package; a fifth would refuse the module in its package manager's
|
||||
words.
|
||||
- **Escalation as a checked fact.** The agent module's write under `/etc` relies on the operator
|
||||
account's passwordless `sudo`, true on all four and checked by nothing. A machine without it refuses
|
||||
the render in the tool's own words; making escalation a reported capability is design 38's to decide.
|
||||
- **An automated switch on exhaustion.** The readings are kept from WP4; the policy is a later record.
|
||||
- **Workers and the mesh's own sessions as consumers.** The manager knows them from WP3; the consumers do
|
||||
not exist yet ([to-be 15](15-the-agent-session.md)).
|
||||
- **Whether a refresh token is single-use.** WP4 may measure it; the design holds either way.
|
||||
|
||||
## How this list is kept true
|
||||
|
||||
Each package's proof is run when the package is finished and its line here gains the date and the
|
||||
commit, as design 38 does. A package whose proof fails is not reworded; the failure is recorded under it
|
||||
and the package stays open. When WP2's live proof runs, design 36 moves to `in-progress` with its owning
|
||||
repository; when WP5's does, design 39 does too; and when WP6's does, both move to `implemented`, with
|
||||
the as-is written.
|
||||
@@ -0,0 +1,238 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-catalog, mesh-controller, mesh-host]
|
||||
updated: 2026-10-04
|
||||
decisions:
|
||||
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
|
||||
- 02-DECISIONS/0040-what-a-module-is.md
|
||||
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||
- 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
|
||||
- 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
|
||||
- 02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md
|
||||
- 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/0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md
|
||||
- 02-DECISIONS/0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md
|
||||
---
|
||||
|
||||
# 42. The machines' modules, in order
|
||||
|
||||
The order in which the modules of [research 026](../../01-RESEARCH/026-the-graphical-session-as-modules/00-overview.md)
|
||||
and [research 027](../../01-RESEARCH/027-the-system-layer-as-modules/00-overview.md) are built and rolled
|
||||
out, as the operator set it on 2026-10-04.
|
||||
|
||||
Three phases, in order, each built from the bottom up (the most core module first):
|
||||
|
||||
1. the modules every machine shares;
|
||||
2. those both workstations share;
|
||||
3. those of one machine model.
|
||||
|
||||
The shell came first ([to-be 41](41-the-shell-and-the-accounts-environment.md)). Each module's
|
||||
definition, improvements and tools follow the research. A position that needs a new mechanism (a new
|
||||
seat, a gated assignment, generalised contributions) gets its record when its first module needs it,
|
||||
not before. Modules that need none go ahead now.
|
||||
|
||||
## How every module moves
|
||||
|
||||
1. Written in the catalogue, with its tools and their tests, and checked by the controller's catalogue
|
||||
check.
|
||||
2. Merged, which builds it.
|
||||
3. Assigned to **the first workstation, the proving machine**, and pushed. Its tools and files are
|
||||
proven there.
|
||||
4. Then assigned to every other machine it applies to, and pushed.
|
||||
|
||||
The operator delegated the go-ahead for each step on 2026-10-04 ("non-important decisions, easily
|
||||
reversed"). Each step is reported.
|
||||
|
||||
**Adopting is also improving** (research 026, 027 overviews): every module lists what it fixes over
|
||||
today, and leaves no predecessor copy of what it now owns.
|
||||
|
||||
## Phase 1 — every machine
|
||||
|
||||
In order:
|
||||
|
||||
| | module | owns | improves |
|
||||
|---|---|---|---|
|
||||
| 1 | `sudo` | the operator account's escalation, as a drop-in it owns | declares what three modules' tools assume and nothing stated |
|
||||
| 2 | `localization` | locale, time zone, console keymap | one machine on another zone and keymap |
|
||||
| 3 | `time-sync` | timesyncd and its servers | two different daemons across four machines |
|
||||
| 4 | `pacman` | the package manager's configuration, mirrors and their refresh, cache cleaning | mirrors generated once and never again; caches never cleaned |
|
||||
| 5 | `logrotate` | the timer and base configuration | rotation running on one machine of four |
|
||||
| 6 | `avahi` | the daemon | on all four, owned by none |
|
||||
| 7 | `systemd` | the service manager's tools (to-be 41 WP4) | built, assigned nowhere |
|
||||
| 8 | `docker` | the runtime's packages, base configuration, group | four configurations, one owner on one machine |
|
||||
| 9 | `ssh-client` | everything under `~/.ssh` (research 027/03) | a predecessor's entries winning over the mesh's; stale keys |
|
||||
| 10 | scripts | the operator's own scripts, shared and per role (research 027/03) | under no version control, copied by hand |
|
||||
| 11 | `kernel` | kernel, microcode, boot entries | two machines without microcode |
|
||||
|
||||
`systemd`, `pacman` and `docker` hold the three seats that apply resources
|
||||
([ADR 0207](../../02-DECISIONS/0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md)):
|
||||
every module that declares a service, a package or a container depends on them being held on its node.
|
||||
They go on every machine before the rest, and once they have, an unmet dependency is refused rather
|
||||
than reported.
|
||||
|
||||
`kernel` is last because a mistake in it costs a boot. `docker` stays a module without the runtime seat
|
||||
until ADRs 0165 and 0166 are accepted.
|
||||
|
||||
## Phase 2 — both workstations
|
||||
|
||||
In order:
|
||||
|
||||
1. `fonts`;
|
||||
2. `xorg` with autorandr;
|
||||
3. `lemurs`;
|
||||
4. `i3`;
|
||||
5. `xterm`;
|
||||
6. the theme module;
|
||||
7. `picom`, `rofi`, `dmenu`, `dunst`, the lock module, `xclip`, the clipboard manager, `feh` and
|
||||
`i3status-rust`;
|
||||
8. `gnome-keyring`;
|
||||
9. `docker-compose`, `snapd`, `flatpak`, `cups`, `bluetooth`.
|
||||
|
||||
The seats, gating, contributions and session start are [ADR 0208](../../02-DECISIONS/0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md).
|
||||
|
||||
## Phase 3 — one machine model
|
||||
|
||||
The laptop's hardware module (vendor daemon, GPU mode, charge limit, logind, brightness and vendor keys)
|
||||
and `memory-pressure` (research 027/03).
|
||||
|
||||
## How it is checked
|
||||
|
||||
Each module's own tests and the catalogue check, at merge. On the proving machine, each tool answered
|
||||
through the mesh and each owned file checked in place, before any other machine is assigned. This
|
||||
document's tables are updated as each module lands.
|
||||
@@ -43,6 +43,8 @@ document is written and this one's status becomes `implemented`.
|
||||
| [`32-what-a-module-declares.md`](32-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md), [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md), superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) |
|
||||
| [`37-the-operators-machine.md`](37-the-operators-machine.md) | **In progress.** Every configurable thing on a node is a module, the home included; one default per module varied by settings or kept regions; roles a machine has once as seats with tool contracts; one tool runtime per node on the host side | [ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md), [0174](../../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md), [0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), [0176](../../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md), [0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md) |
|
||||
| [`38-building-the-operators-machine.md`](38-building-the-operators-machine.md) | **In progress.** The work of design 37 as packages: the runtime serves many modules, the controller composes one per node, the console becomes its serving mode, the packet filter moves first, then the shell and the service manager — tested on the live mesh by the operator's decision | [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), [0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md), [0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md) |
|
||||
| [`41-the-shell-and-the-accounts-environment.md`](41-the-shell-and-the-accounts-environment.md) | **In progress.** The shell and the account's environment as modules: an environment module every module contributes variables and `PATH` entries to, shell code contributed to the login shell in named slots, the prompt and plugins as modules, the host giving a login back, and the service manager's module finished | [ADR 0203](../../02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md), [ADR 0204](../../02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md), [ADR 0205](../../02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md) |
|
||||
| [`42-the-machines-modules-in-order.md`](42-the-machines-modules-in-order.md) | **In progress.** The order the machines' modules of research 026 and 027 are built and rolled out: every machine's first (sudo, localization, time sync, pacman, logrotate, avahi, systemd, docker, `~/.ssh`, scripts, kernel), then both workstations', then one machine model's; each proven on one workstation before the rest | [ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md), [ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md), [ADR 0205](../../02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md) |
|
||||
|
||||
## Not yet written
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
status: resolved
|
||||
opened: 2026-09-26
|
||||
located-in: [mesh-controller internal/catalogue, mesh-sdk src/provisioner, mesh-catalog modules/minio]
|
||||
fixed-by: 02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md
|
||||
fixed-by: 02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md
|
||||
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||
---
|
||||
|
||||
@@ -64,7 +64,7 @@ compares it to what the provider will actually create. The one wrong instance wa
|
||||
bucket in its own configuration against the one the provider would create is a check that could
|
||||
exist today, for any interface, without the mechanism above.
|
||||
|
||||
## Answered, 2026-10-02 — [ADR 0201](../../02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md)
|
||||
## Answered, 2026-10-02 — [ADR 0202](../../02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md)
|
||||
|
||||
The channel is the provider's own `serves` block, which may now name the consumer the mesh is
|
||||
serving: `${consumer:as}` and `${consumer:as:dns}`. The mesh fills it once, where it knows who the
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-09-30
|
||||
located-in: [mesh-host internal/apply (no removal for an archive)]
|
||||
fixed-by:
|
||||
- mesh-host#90
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -54,3 +55,15 @@ argue for elsewhere: the mesh gives back what it found.
|
||||
A module with an archive is assigned, pushed, unassigned and pushed again; what the archive put on the
|
||||
machine is gone, anything that was in the directory beforehand is still there, and the apply that
|
||||
removed it applied everything else in the same declaration.
|
||||
|
||||
## Resolved
|
||||
|
||||
*2026-10-04.* Hit again live the same day: a race between two pushes delivered a declaration without
|
||||
a just-assigned module, and removing its tools bundle stopped a workstation's apply until the next push.
|
||||
mesh-host#90 records what an archive unpacked: its files, the directories it made, whether the host
|
||||
made the target and its parents. Undeclaring removes exactly those, never a file it did not place and
|
||||
never a directory that was there before, and a failed removal is reported, never fatal.
|
||||
|
||||
An archive recorded before the change learns its list from its own bytes on the next apply while it is
|
||||
still declared. One already orphaned is left in place, said and forgotten. Applying an archive no longer
|
||||
deletes files the mesh did not place in its target directory (ADR 0030).
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-10-04
|
||||
located-in: [mesh-host internal/apply/apply.go, mesh-host internal/apply/schedule.go]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 224 — An apply arriving during a maintenance window reopens it, by recreating the container the window is holding still
|
||||
|
||||
## What was observed
|
||||
|
||||
Reviewing [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md)'s
|
||||
`while-stopped` before merging it, 2026-10-04. Found by reading, not by running.
|
||||
|
||||
A scheduled step may hold its module's containers still while it runs. The host stops them, runs
|
||||
the step, starts them again. Nothing tells the **apply** that a window is open, and the apply's
|
||||
rule for a container it finds stopped is to replace it:
|
||||
|
||||
```
|
||||
case existed && (before.Spec == want || legacy) && before.Running && len(reasons) == 0:
|
||||
out.Action = "unchanged"
|
||||
case existed:
|
||||
rm -f
|
||||
```
|
||||
|
||||
`before.Running` is false for a container a window is holding, so the second branch takes it:
|
||||
the container is removed and recreated, **running**, in the middle of the step that required it
|
||||
to be still.
|
||||
|
||||
## Why it matters
|
||||
|
||||
For the store, which is what the field was built for, the chain is: a push lands at 03:30 → the
|
||||
apply recreates the registry → the registry accepts an upload from a build running at the same
|
||||
time → `garbage-collect`, already past its mark phase, sweeps the blob that upload just wrote.
|
||||
The image is then in the store with a layer missing, and the build that made it reported success.
|
||||
|
||||
Two things have to coincide, so it is not likely. It is also not rare enough to leave unsaid: the
|
||||
mesh pushes on every merge, at any hour, and a collection over a store this size is minutes rather
|
||||
than seconds.
|
||||
|
||||
**The general shape is the one that matters.** `while-stopped` is the first thing in the mesh that
|
||||
makes a container's stopped state *intentional*. Everything else in the host reads "stopped" as
|
||||
"broken, fix it", which is right everywhere else and wrong here. Any future use of the field
|
||||
inherits this.
|
||||
|
||||
## What this is not
|
||||
|
||||
Not a regression. The store has never collected anything, so nothing is worse than it was; this
|
||||
is a hole in something new rather than something that broke.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Should a window take the apply lock?** The daemon already serialises applies with `applying`
|
||||
and, across processes, with `store.Lock`. A window that held it would make the race impossible.
|
||||
The cost is that a push arriving mid-window waits for minutes, and a push that waits is what
|
||||
[issue 185](../185-a-refused-membership-publish-stops-the-controller/00-report.md)'s
|
||||
family of outages looked like from outside.
|
||||
- **Or should the apply learn that a container is held?** Narrower: the scheduler says which
|
||||
containers a window currently holds, and `applyContainer` reports those unchanged instead of
|
||||
recreating them. Nothing blocks, and the apply tells the truth for the minutes it matters —
|
||||
at the cost of a second source for "is this container meant to be running".
|
||||
- Either way: should the *report* say a window is open, so a machine that looks half-stopped at
|
||||
03:31 reads as working rather than broken?
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-04
|
||||
located-in: [mesh-host internal/apply, mesh-controller internal/catalogue]
|
||||
fixed-by: mesh-controller#263
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 225 — A provisioner cannot read the grant secrets since its code left the container, and every consumer of it is unserved
|
||||
|
||||
## What was observed
|
||||
|
||||
On the control machine, 2026-10-04, found while looking at why one app was restarting:
|
||||
|
||||
```
|
||||
[mongodb] [provisioner:mongodb-database] mesh_novox_photos: secret not readable yet
|
||||
(/var/lib/mongodb/grants/novox.photos.secret):
|
||||
Error: EACCES: permission denied, open '/var/lib/mongodb/grants/novox.photos.secret'
|
||||
```
|
||||
|
||||
**4330 times, every five seconds, since 01:30:20.** The consequence is not a log line: the
|
||||
provisioner never reads the password, so it never creates the user, so the consumer never
|
||||
connects —
|
||||
|
||||
```
|
||||
UserNotFound: Could not find user "mesh_novox_photos" for db "admin"
|
||||
```
|
||||
|
||||
— and the app crash-loops. Two consumers on this machine are in that state.
|
||||
|
||||
## Why
|
||||
|
||||
The grant secrets are what the mesh seals for each consumer and the host unseals beside the
|
||||
provider's contributions file. They are written `-rw------- root root`, which was right while a
|
||||
module's own code ran in a container as root.
|
||||
|
||||
[ADR 0198](../../02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)
|
||||
moved a module's long-running code out of its container and under the node's runtime, which runs
|
||||
as the operator's account. The provisioner is now that account; the secret is still root's. The
|
||||
timestamps say it exactly: the files are dated 2026-09-26, the first refusal is 01:30:20 on the
|
||||
day the runtime rolled.
|
||||
|
||||
**Nothing reports it.** The machine applies cleanly and reads as current; the provisioner says
|
||||
`secret not readable yet`, whose wording is for a real and ordinary race on the first pass — the
|
||||
host has not written the file yet — and which is indistinguishable, in the log, from a permanent
|
||||
refusal. Four thousand occurrences of a message that means "wait a moment" is the shape to
|
||||
recognise.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
This is every provider that provisions. The grant secret is the one file the sdk's harness reads
|
||||
for every consumer, so a provider that cannot read it serves nobody — and says so only in a line
|
||||
that reads like patience.
|
||||
|
||||
It is also the general question the runtime move leaves: **what the mesh seals for a module is
|
||||
owned for the shape that module's code used to have.** Each module whose code moved is a module
|
||||
whose files may now be unreadable to it, and ownership is the mesh's to state, not the module's
|
||||
to work around.
|
||||
|
||||
## What this is not
|
||||
|
||||
Not caused by [ADR 0202](../../02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md)
|
||||
or [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md), which landed two
|
||||
to three hours after the first refusal. Those rebuilt the two affected consumers, which recreated
|
||||
their containers and made a silent fault visible as a restarting one. The dates are above.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Who owns a grant secret now — the module's account, as `secrets-owner` already says for a
|
||||
module's own secrets? Then the host writes it so, and this is a one-line statement in the
|
||||
declaration rather than a convention.
|
||||
- Should `secret not readable yet` stop saying "yet" after the first few passes? A message that
|
||||
is right once and wrong four thousand times is a message that hides its own meaning.
|
||||
- Which other modules' files did the runtime move leave behind? The sweep is the same question
|
||||
for every path the mesh writes for a module: directories, bundles, received files.
|
||||
|
||||
## Resolved, 2026-10-04
|
||||
|
||||
A grant secret is composed with the account that will read it: the node's account where the
|
||||
module's code is a bundle the runtime runs, its declared `secrets-owner` where it is still a
|
||||
container, root where it says neither. The same rule the module's own secrets already followed,
|
||||
reaching the other kind of secret the mesh writes for a module (mesh-controller#263).
|
||||
|
||||
`givenTo` could not have reached these: it claims the files a bundle's *words* name, and the
|
||||
harness composes a grant secret's path from the contributions file, which no word names.
|
||||
|
||||
And the harness stops calling a permanent refusal a race (mesh-sdk#22, 0.1.9). After about a
|
||||
minute it says so plainly, rarely rather than every five seconds, and names what to look at —
|
||||
who owns the file and who the process runs as. That is the half that cost three hours.
|
||||
|
||||
**How it was checked:** on the control machine, after the push — the grant secrets belong to the
|
||||
operator's account, **zero `EACCES` since 12:30:10** where there had been 4330, both users were
|
||||
created, and `mongodb-server` logs `Authentication succeeded` for each.
|
||||
|
||||
It also uncovered [issue 232](../232-a-consumer-authenticates-against-a-database-its-user-does-not-live-in/00-report.md),
|
||||
which this fault had been hiding: with no user anywhere, "not found in admin" was a complete
|
||||
account of *this* issue and said nothing about the consumer asking the wrong database.
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-04
|
||||
located-in: [mesh-controller cmd/mesh-controller/collect.go, mesh-controller internal/inventory/collection.go]
|
||||
fixed-by: mesh-controller#263
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 226 — The store's sweep stops at the first reference recorded with an address, so it collects nothing at all
|
||||
|
||||
## What was observed
|
||||
|
||||
The first live run of [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md)'s
|
||||
sweep, 2026-10-04, printed on every build:
|
||||
|
||||
```
|
||||
the artifact store kept 127.0.0.1:5100/mesh-tools/build@sha256:0de48cd3…, so nothing more was
|
||||
asked of it: 127.0.0.1:5100/mesh-tools/build@sha256:0de48cd3… is not a reference into the
|
||||
mesh's artifact store
|
||||
1681 more to collect; the next build asks again
|
||||
```
|
||||
|
||||
Nothing is collected, and nothing ever will be. The store holds 1681 artifacts the mesh no longer
|
||||
keeps and the feature that exists to remove them is inert.
|
||||
|
||||
## Why
|
||||
|
||||
Two correct decisions meeting badly.
|
||||
|
||||
**A reference recorded before references were kept without an address** is
|
||||
`127.0.0.1:5100/<path>@sha256:…` rather than `artifact-store://<path>@sha256:…`
|
||||
([04-ISSUES/102](../102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)).
|
||||
`LetGo` rightly refuses to compose a delete for a reference whose shape it does not recognise —
|
||||
that refusal is what keeps the sweep from reaching something that is not the mesh's.
|
||||
|
||||
**The sweep stops at the first refusal**, because "a store that refuses one refuses all of them"
|
||||
— deletion disabled, the store down, the network gone — and pushing through would mean a hundred
|
||||
identical failures in front of whoever was building something. That reasoning is right for the
|
||||
store refusing. It is wrong for *this* record being unreadable.
|
||||
|
||||
So one old record, early in the oldest-first order, halts the whole sweep for ever.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
**A guard that cannot tell "I will not ask about this" from "it would not answer" stops the wrong
|
||||
amount of work.** The two deserve opposite responses: skip one, abandon the other. Collapsing
|
||||
them into "an error" is how a bounded, cautious loop becomes a loop that does nothing — and it
|
||||
reports the right number while doing it, which is what made it look healthy.
|
||||
|
||||
## What a fix has to settle
|
||||
|
||||
- A reference the sweep cannot address is **skipped, and the sweep goes on** — it is a fact about
|
||||
that record, not about the store.
|
||||
- `Recorded()` already normalises the old form to the kept one, and is what the rest of the mesh
|
||||
uses for exactly these references. The sweep should normalise before asking rather than refuse.
|
||||
- Only a refusal *by the store* ends a sweep.
|
||||
- **How it is checked:** a sweep over records holding one address-recorded reference and one kept
|
||||
one collects the second; a sweep against a store that refuses stops at the first.
|
||||
|
||||
## Resolved, 2026-10-04
|
||||
|
||||
Two changes, deliberately separate.
|
||||
|
||||
**Normalising moved to where the provenance is known.** Every reference the sweep handles came
|
||||
from a build record, so every one is the mesh's own and `Recorded` may read an address-era
|
||||
reference as the kept one. Not in `LetGo` — it cannot tell `docker.io` from the mesh's store, and
|
||||
an attempt to put it there was caught at once by the test that says a foreign reference is never
|
||||
asked about. The guard stays strict; the records speak the one vocabulary.
|
||||
|
||||
**A reference the sweep will not address is `ErrNotOurs`:** skipped, not marked collected, never
|
||||
a reason to stop. Only the store refusing ends a sweep.
|
||||
|
||||
**How it was checked:** the first build after the roll-out printed *"the artifact store let go of
|
||||
200 artifact(s) the mesh no longer keeps — 1126 more to collect; the next build asks again"*.
|
||||
Two hundred is the per-sweep bound working as intended; the backlog is falling with every build
|
||||
instead of standing at 1681 for ever.
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-04
|
||||
located-in: [mesh-catalog modules/photos]
|
||||
fixed-by: mesh-catalog#263, mesh-controller#263
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 227 — The photo app's admin client asks for port 80, which the reverse proxy holds, so it cannot start
|
||||
|
||||
## What was observed
|
||||
|
||||
Applying the control machine, 2026-10-04:
|
||||
|
||||
```
|
||||
applying "photos.admin-client": starting container photos-admin-client:
|
||||
failed to bind host port 0.0.0.0:80/tcp: address already in use
|
||||
```
|
||||
|
||||
Port 80 on that machine belongs to the reverse proxy (`mesh-route-proxy`, confirmed with `ss`),
|
||||
which is the whole arrangement: the proxy holds the public ports and every module is reached
|
||||
through it. A module that publishes 80 itself can never start beside it.
|
||||
|
||||
Everything else on the machine applied; this one resource fails every pass.
|
||||
|
||||
## How it surfaced
|
||||
|
||||
`photos` had been pinned at a commit from 2026-09-28 and was rebuilt to `main` on 2026-10-04 —
|
||||
forced by [ADR 0202](../../02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md)'s
|
||||
refusal of its transcribed bucket name. The admin client is one of the changes that came with the
|
||||
rest of `main`. The rebuild did not create the conflict; it delivered it.
|
||||
|
||||
**A module pinned months behind carries whatever its branch gained, all at once, the first time
|
||||
something makes it move.** That is the cost of a pin, and it is paid in full rather than
|
||||
gradually.
|
||||
|
||||
## What a fix has to settle
|
||||
|
||||
- Which port the admin client should ask for, or whether it should be reached through the proxy
|
||||
like everything else and publish nothing.
|
||||
- Whether a module declaring a port the machine's proxy already holds should be refused when it
|
||||
is composed, rather than failing on the machine every pass. The mesh assigns ports
|
||||
([ADR 0038](../../02-DECISIONS/0038-the-mesh-assigns-the-port.md)); a fixed 80 beside a proxy is
|
||||
a statement it could check.
|
||||
|
||||
## Resolved, 2026-10-04
|
||||
|
||||
The three photo modules publish the endpoint they declare — `4001:80`, `4012:80`, `4013:80` —
|
||||
so the software's own port reaches the machine at the port the mesh assigned, and nothing asks
|
||||
for 80.
|
||||
|
||||
**The rule, rather than three repairs.** A container may publish only a port its module declares:
|
||||
the short form `"80"` means *publish what the software calls 80*, and the mesh fills in the
|
||||
machine's half from the port it assigned — which it can only do for a port the module declared.
|
||||
Four modules publish 80 quite safely because they declare 80. The difference is the declaration,
|
||||
not the number. A catalogue-wide test in mesh-controller#263 says so, and names all three
|
||||
offenders against the catalogue as it was.
|
||||
|
||||
**How it was checked:** `photos-admin-client` has been up since the push, where before it failed
|
||||
on every pass.
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
status: located
|
||||
opened: 2026-10-04
|
||||
located-in:
|
||||
- mesh-host
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 228 — A login the mesh set is never given back, and undeclaring one stops the node applying
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-04. Before the shell module of to-be 38 WP5 was assigned anywhere, a review traced what the
|
||||
host does with the `user` shape the module declares (the operator account, with the login shell zsh)
|
||||
on three events: first assign, a later push, and unassign.
|
||||
|
||||
1. **Undeclaring a `user` stops the node applying anything, for good.**
|
||||
- The host's removal has no case for a `user`, so the orphaned record fails with "no way to remove".
|
||||
- Orphans are removed before the declaration's first resource, and that failure aborts the apply.
|
||||
- The record stays in the host's store, so every later apply fails the same way.
|
||||
|
||||
This was reproduced in a throwaway test against the host's code: the apply produced no outcomes,
|
||||
and an unrelated file in the same declaration was not written. Renaming the resource's id has the
|
||||
same effect. A showcase module carries a `user` today and is exposed to it too.
|
||||
2. **The shell the account had is never recorded.** [ADR 0176](../../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)
|
||||
§2 says the host "gives back [the login shell] when the holding moves". The host keeps nothing to
|
||||
give back.
|
||||
3. **A login shell is set whether or not it exists.** A failed package install does not stop the
|
||||
resources after it. `usermod --shell` on the distribution only warns about a missing or
|
||||
non-executable shell, and succeeds. The host's read-back compares the user database's string,
|
||||
which matches. So an account can be pointed at a shell that is not there, and console, ssh and
|
||||
display-manager logins then fail. No machine hit this, because zsh was already installed on all
|
||||
four.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
Unassigning any module with a login in it, the case the mesh promises is ordinary, wedges the
|
||||
machine's applies until a person edits the host's store. It is the same class of failure as an
|
||||
earlier archive that could not be removed: a shape the host can create and cannot take away.
|
||||
|
||||
## Located
|
||||
|
||||
mesh-host, `internal/apply`: `remove()` has no `user` case, and `applyUser` neither records the shell
|
||||
it replaced nor checks the shell it sets. The fix is set out in
|
||||
[to-be 41](../../03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md) WP1:
|
||||
|
||||
- a removal that never deletes an account;
|
||||
- the login shell given back, if it is still the one the mesh set and the recorded one still exists;
|
||||
- a shell refused before it is set unless it is executable and listed among the machine's shells
|
||||
(a shell that refuses logins need only be executable, since the distribution does not list it and
|
||||
the controller's own account uses one).
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-10-04
|
||||
located-in: []
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 229 — A rollout cannot be followed through the mesh's tools, so an agent goes round them
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-04, rolling out to-be 41. An agent drove the rollout through the mesh's MCP tools: the
|
||||
controller seat's `status`, `plans`, `command`, and the forge's merge. Four times it left those tools
|
||||
and posted JSON-RPC by hand to the node console's HTTP endpoint with `curl`:
|
||||
|
||||
1. **To wait for a plan.** `plans` answers once, with prose. Nothing waits for a plan to reach a tier,
|
||||
finish or fail. An agent's tools cannot be called from a shell loop, so the only way to be told
|
||||
when a plan moved was a background `curl` loop polling the console every twenty seconds and
|
||||
matching the plan's line with `grep`.
|
||||
2. **To read `status`.** `status` answers a paragraph of prose (the bus's user list), then a JSON
|
||||
document, both inside one string. Picking out `behind`, `waiting` and `reported` took a script
|
||||
that cut the string at the first brace and parsed the rest.
|
||||
3. **To read one module out of `module list`,** whose output was too long to read whole for one line.
|
||||
4. **To call a tool that arrived after the agent's session began.** The modules rolled out in that same
|
||||
session added `node-login-shell.execute` and `zsh.zsh_config` to one machine. The agent's MCP
|
||||
connection had been opened before the console moved to discovery ([ADR 0195](../../02-DECISIONS/0195-the-meshs-tools-are-found-by-address-not-announced-whole.md)).
|
||||
It still held the flat catalogue the console announced then, which lacks both the new verbs and the five discovery tools (`mesh_call` among
|
||||
them) the console announces now. Clearing a session does not reconnect its MCP servers, and the
|
||||
console never sends a list-changed notice, so nothing told the client its list was stale. The agent
|
||||
posted `mesh_machine` and `mesh_call` by hand. Reconnecting the server would have given it the
|
||||
discovery tools, which reach any tool by address the moment it exists.
|
||||
|
||||
The calls were authorised, because the console is the operator's own surface. But each is a raw call
|
||||
the mesh's tools were meant to make unnecessary ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
|
||||
puts the controller's verbs behind the seat). Each is also a script that breaks silently when a
|
||||
sentence in the prose changes.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
Every rollout an agent drives has the same shape: merge, wait for a plan, push, wait for reports,
|
||||
check `status`. When the tools answer only once and only in prose, every agent writes its own poller
|
||||
and its own parser. Those are invisible to review, different each time, and wrong the first time the
|
||||
wording moves. An agent that cannot wait also tends to act early, which is the opposite of what a
|
||||
rollout needs.
|
||||
|
||||
## What a fix has to settle
|
||||
|
||||
- A way to **wait** on the mesh's own progress. For example, `plans` and `status` could take a plan
|
||||
or node and a bound, and answer when it moves or the bound passes. Or a verb could follow one plan
|
||||
to its end.
|
||||
- **Structured answers** from the controller's verbs, with the prose as a field beside the data, not
|
||||
around it.
|
||||
- Whether `command`'s generic answer should take a filter, or whether the verbs it is used for most
|
||||
(`module list`, `node show`) deserve verbs of their own.
|
||||
- **A client is told when the console's own surface changes.** The console announces `listChanged`
|
||||
and sends the notice when what it lists changes, for example after an upgrade that changes its
|
||||
tools. A long-running session then never keeps a list the console no longer serves. Discovery
|
||||
already makes every module's tools reachable without the list changing.
|
||||
|
||||
How each is checked belongs to the record that settles it.
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-10-04
|
||||
located-in:
|
||||
- mesh-host
|
||||
- mesh-controller
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 230 — A host that hands over to a newer one loses its report, and a plan waits for it for ever without saying so
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-04, rolling out to-be 41. A new host build and a new controller were merged together. The
|
||||
controller's plan built build-agent and sent every machine a fresh declaration, which also delivered
|
||||
the new host. Three of the four machines logged, within the same second:
|
||||
|
||||
```
|
||||
host 4bd7df099757 is delivered; standing aside so the launcher runs it
|
||||
applied 546 resource(s)
|
||||
applied, and could not tell the mesh: reporting: context canceled
|
||||
nox-mesh-host-launch: running /usr/lib/nox-mesh-host/versions/4bd7df099757/nox-mesh-host
|
||||
```
|
||||
|
||||
The new host came up and waited for its next declaration. The mesh never heard that the old one had
|
||||
applied.
|
||||
|
||||
The plan then sat at "tier 1 built; waiting for build-agent on [three machines] to be applied", and
|
||||
everything the mesh said about it read as healthy:
|
||||
|
||||
- `status` showed it as `rolling` with `"late": false`;
|
||||
- `plans` printed "for 0s" on every look, so the wait never appeared to grow;
|
||||
- the three machines' reports showed `current: false`, which reads like a machine that is merely slow.
|
||||
|
||||
Nothing logged, alerted or counted the wait. It was found because a person asked twice for the plan's
|
||||
state, and the cause was found by reading a machine's own journal. A push to each of the three machines
|
||||
released it: each new host applied and reported, and the plan moved on.
|
||||
|
||||
The same day, a second way to lose a report showed up. Assigning modules with tools to a workstation
|
||||
changed the bus's user list, which the control machine's declaration carries. Applying it replaced the
|
||||
bus's container, which cut every machine off for about fifteen seconds. The control machine itself
|
||||
then logged `applied, and could not tell the mesh: reporting: nats: connection closed`. The report was
|
||||
lost because the bus restarted under the apply that restarted it.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
**Every genuine host upgrade loses one report.**
|
||||
[Issue 163](../163-a-delivered-host-stood-aside-on-every-push-and-reported-nothing/00-report.md) fixed
|
||||
the host that stood aside on every push for the version it already ran. It named the mechanism, that
|
||||
standing aside cancels the context the report is published with. That fix made standing aside
|
||||
happen only for a real new version, but left the mechanism in place. So whenever a host build reaches
|
||||
a machine, that apply's report is lost.
|
||||
|
||||
**And the mesh cannot tell a stuck wait from a slow one.** A plan that waits on a report that will
|
||||
never come waits for ever, and nothing about it changes:
|
||||
|
||||
- its age does not grow ("for 0s");
|
||||
- `late` stays false;
|
||||
- nothing logs, emits an event or alerts.
|
||||
|
||||
This is [issue 187](../187-the-mesh-tells-nobody-when-it-stops-working/00-report.md)'s class of fault
|
||||
again, *the mesh tells nobody when it stops working*, now in the rollout machinery that every merge
|
||||
goes through. The operator's rule from issue 163 applies: if an answer has not come in the time an
|
||||
answer takes, something is wrong, and the mesh must say so itself.
|
||||
|
||||
## What a fix has to settle
|
||||
|
||||
1. **A report survives whatever its own apply restarts.** The host publishes its report and has it
|
||||
acknowledged before it stands aside. It retries a report the bus dropped once the link is back.
|
||||
Failing that, the new host should report the declaration it took over,
|
||||
naming the apply its predecessor finished. A lost report must be impossible, not merely unlikely.
|
||||
2. **A plan's wait has an age and a bound.**
|
||||
- "for 0s" must be the real time since the wait began.
|
||||
- A wait past a bound, set by how long an apply takes rather than by a guess, makes the plan
|
||||
`late`.
|
||||
3. **Late is said where people and agents look.**
|
||||
- It is said in `status` and in `plans`.
|
||||
- It is logged as a warning by the controller.
|
||||
- It is emitted as an event under the controller seat, so something can alert on it.
|
||||
4. **A plan waiting on a machine the mesh has stopped hearing from** says that, by name, instead of
|
||||
waiting. The machine's heartbeat already tells the controller it is alive. A live machine with an
|
||||
unacknowledged declaration is the stuck case itself.
|
||||
|
||||
How each is checked belongs to the fix. For the host: a delivered upgrade, applied, is reported. For
|
||||
the controller: a plan whose machine never reports turns `late` within its bound, and says so in
|
||||
`status`, the log and an event.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-10-04
|
||||
located-in:
|
||||
- mesh-controller
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 231 — A misspelled placeholder is written out as text
|
||||
|
||||
## What was observed
|
||||
|
||||
2026-10-04, while studying the predecessor's failures. A manifest was checked whose one file holds four
|
||||
placeholders: `${shel:zsh:first}` (a misspelled namespace), `${setting:Undeclared}` (a setting the module
|
||||
does not declare), `${machnie:address}` (a misspelled namespace) and `${XDG_CACHE_HOME:-x}` (shell
|
||||
syntax, which must pass through). The catalogue check, which runs the same functions registration does,
|
||||
answered `ok`.
|
||||
|
||||
The controller fills each namespace it knows with its own pattern (`machine`, `setting`, `bound`, `dir`,
|
||||
`port`, `secret`, `environment`, `shell`, …). A word in that shape that no pass consumes is left in the
|
||||
file as it was written. A misspelling therefore reaches a machine as literal text, in a configuration
|
||||
file that then reads it as a value.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
This is exactly the predecessor's failure: an unresolved template variable in a destination path
|
||||
installed green, and a literal `${...}` path stood under `/etc/ssl` until somebody looked. The mesh's
|
||||
namespaced placeholders were meant to end it ([ADR 0164](../../02-DECISIONS/0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md)),
|
||||
and they do for every name spelled right.
|
||||
|
||||
## What a fix has to settle
|
||||
|
||||
After every pass, a final sweep refuses any remaining `${<word>:` whose word is a lower-case
|
||||
namespace-shaped token, naming the module, the field and the token, at the catalogue check and at
|
||||
composition. Shell syntax (`${NAME:-…}`, `${(%):-…}`, `${1:-.}`) is not namespace-shaped and passes. So
|
||||
does contributed shell code, which no pass reads (ADR 0204). A setting a module uses but does not
|
||||
declare is refused the same way.
|
||||
|
||||
How it is checked: the controller's test with the four placeholders above, three refused by name and
|
||||
one passed through.
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-04
|
||||
located-in: [mesh-catalog modules/photos]
|
||||
fixed-by: mesh-catalog#264
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 232 — A consumer authenticates against a database its user does not live in, and one fault hid it behind another
|
||||
|
||||
## What was observed
|
||||
|
||||
Fixing [issue 225](../225-a-provisioner-cannot-read-the-grant-secrets-since-its-code-left-the-container/00-report.md)
|
||||
on the control machine, 2026-10-04. With the grant secrets readable again the provisioner created
|
||||
both consumers' users at once, and one consumer still could not connect:
|
||||
|
||||
```
|
||||
Authentication succeeded | user: mesh_novox_invoice | authDb: mesh_novox_invoice
|
||||
Authentication succeeded | user: mesh_novox_photos | authDb: mesh_novox_photos
|
||||
Authentication failed | user: mesh_novox_photos | authDb: admin
|
||||
UserNotFound: Could not find user "mesh_novox_photos" for db "admin"
|
||||
```
|
||||
|
||||
The provider creates each consumer's user **in that consumer's own database**, which is what the
|
||||
first two lines are. `photos` asks for `admin`. Its sibling `invoicing`, against the same
|
||||
provider, asks for `${bound:mongodb-database:as}` — the name the mesh gave it — and works.
|
||||
|
||||
The password was never the problem: the grant secret and the value in the consumer's environment
|
||||
hash identically.
|
||||
|
||||
## Why it matters beyond this instance
|
||||
|
||||
**One fault wore the other's clothes.** While the provisioner could not read its secrets at all,
|
||||
*no* user existed, so `UserNotFound ... for db "admin"` was a true and complete account of issue
|
||||
225. Fixing 225 is what made the wrong database visible — before that, every symptom pointed at
|
||||
the thing that was already broken, and a second fault behind it was indistinguishable.
|
||||
|
||||
That is the general shape worth keeping: **a fault that explains the symptom is not evidence
|
||||
there is only one.** The check is to fix the first and look again, rather than to close both on
|
||||
one explanation.
|
||||
|
||||
It also says something about the interface. Which database a consumer authenticates against is
|
||||
part of what `mongodb-database` means, and it is spelled out by hand in each consumer. Two
|
||||
consumers of one provider wrote two different answers, and only one was right; nothing compared
|
||||
them. That is the shape [issue 124](../124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md)
|
||||
records for a derived bucket, here for the authentication database —
|
||||
[ADR 0202](../../02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md)'s
|
||||
mechanism is what would remove it, by letting the provider say it once.
|
||||
|
||||
## Resolved
|
||||
|
||||
`photos` asks for `${bound:mongodb-database:as}`, as `invoicing` already did (mesh-catalog#264).
|
||||
**How it is checked:** the module is rebuilt and pushed, and `photos-server` connects — verified
|
||||
on the control machine rather than inferred from the manifest.
|
||||
|
||||
## What this leaves open
|
||||
|
||||
Nothing compares two consumers' idea of one interface. The provider could serve the
|
||||
authentication database as a derived value under ADR 0202 and neither consumer would state it;
|
||||
that is a candidate for the next consumer of this interface, not a repair of this one.
|
||||
Reference in New Issue
Block a user