Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfb2817fe7 | ||
|
|
12742e3a3f | ||
|
|
5b00bdc7af | ||
|
|
d5a96e5c63 | ||
|
|
73e6400802 | ||
|
|
f144ad7be4 | ||
|
|
8394a7bfb1 | ||
|
|
6ac936f170 | ||
|
|
3af4179755 | ||
|
|
dd04729ec5 | ||
|
|
2e5f40c9fa | ||
|
|
2f41b4124d | ||
|
|
9cc00d57ea | ||
|
|
438162b5a5 |
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
status: active
|
||||||
|
initiated: 2026-10-04
|
||||||
|
touches:
|
||||||
|
- 04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md
|
||||||
|
- 04-ISSUES/229-a-rollout-cannot-be-followed-through-the-meshs-tools/00-report.md
|
||||||
|
- 04-ISSUES/230-a-host-that-hands-over-to-a-newer-one-loses-its-report-and-a-plan-waits-for-ever/00-report.md
|
||||||
|
- 04-ISSUES/233-a-host-without-its-package-managers-configuration-refuses-the-declaration-that-would-restore-it/00-report.md
|
||||||
|
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
||||||
|
- 02-DECISIONS/0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md
|
||||||
|
- 02-DECISIONS/0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md
|
||||||
|
- 03-DESIGN/01-to-be/32-what-a-module-declares.md
|
||||||
|
became: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# 028 — The mesh's output channel
|
||||||
|
|
||||||
|
## What
|
||||||
|
|
||||||
|
How the mesh tells its operator what it noticed. The operator's framing: sending notifications
|
||||||
|
is **an output channel for the mesh**. The mesh already knows when a machine stops answering, when
|
||||||
|
a failure repeats and will not fix itself, when a rollout waits for ever. Today it keeps that to
|
||||||
|
itself until someone asks.
|
||||||
|
|
||||||
|
The effort looks at:
|
||||||
|
|
||||||
|
- **the seat:** one, held once for the mesh, that every other part uses to say something to the
|
||||||
|
operator;
|
||||||
|
- **the channels**, each a module: a desktop notification on the machine the operator is at,
|
||||||
|
**Telegram**, a phone push service, chat, mail and others (see [02](02-the-channels.md));
|
||||||
|
- **the routing**, by severity and by where the operator is;
|
||||||
|
- **the life of a message:** deduplicated while it holds, resolved when it stops, acknowledged or
|
||||||
|
silenced by the operator;
|
||||||
|
- **the watcher's watcher:** who tells the operator when the parts that would tell them are the
|
||||||
|
ones that failed.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
[Issue 187](../../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md) is the
|
||||||
|
class: *the mesh tells nobody when it stops working*. [01](01-what-the-mesh-already-knows.md)
|
||||||
|
counts it.
|
||||||
|
- 15 of the 236 issue reports say the fault was found because a person happened to look.
|
||||||
|
- 74 describe something failing silently.
|
||||||
|
|
||||||
|
On the day this effort opened, the mesh knew three things and told nobody:
|
||||||
|
- a workstation had refused every declaration for ninety minutes;
|
||||||
|
- the same workstation had been out of touch for ten minutes after an upgrade;
|
||||||
|
- one failure on the laptop had repeated thirteen times.
|
||||||
|
|
||||||
|
Every one of them was in `status`, for whoever asked.
|
||||||
|
|
||||||
|
The pieces exist. [To-be 32](../../03-DESIGN/01-to-be/32-what-a-module-declares.md) already uses a
|
||||||
|
`telegram-sender` seat as its worked example of a work queue with retention. ADR 0208 made a
|
||||||
|
machine's desktop notifier a node seat with a `send` verb. What is missing is a seat that speaks
|
||||||
|
for the mesh, sources that call it, and channels that deliver.
|
||||||
|
|
||||||
|
## What it touches
|
||||||
|
|
||||||
|
- **The controller**, which would become the first source of what it already computes for `status`.
|
||||||
|
- **The node-notifier seat**, which would become one channel among several.
|
||||||
|
- **Issues 187, 229, 230 and 233**, each of which ends in "and nothing said so".
|
||||||
|
- **ADR 0210**, because a channel extends the output seat through a contribution, and therefore
|
||||||
|
depends on it.
|
||||||
|
|
||||||
|
## Documents
|
||||||
|
|
||||||
|
1. [What the mesh already knows](01-what-the-mesh-already-knows.md): the evidence, and the events
|
||||||
|
that exist.
|
||||||
|
2. [The channels](02-the-channels.md): the candidates, Telegram first, weighed on the same questions.
|
||||||
|
3. [Open questions](03-open-questions.md): the seat, routing, life of a message, the watcher's
|
||||||
|
watcher, what may leave the mesh.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# 01 — What the mesh already knows, and who hears it
|
||||||
|
|
||||||
|
## The count
|
||||||
|
|
||||||
|
Over the 236 issue reports in `04-ISSUES/`, on the day this effort opened:
|
||||||
|
|
||||||
|
- **15** say, in some wording, that a person found the fault by looking: "nobody was told",
|
||||||
|
"nothing logged / said / alerted / emitted", "a person asked", "found by a person". Five of them
|
||||||
|
are still open.
|
||||||
|
- **74** describe something that failed silently.
|
||||||
|
|
||||||
|
The search was a word match over the reports' text, so it undercounts reports that tell the same
|
||||||
|
story in other words. It never overcounts by much: each of the fifteen was read.
|
||||||
|
|
||||||
|
The fifteen fall into three groups:
|
||||||
|
|
||||||
|
- **The mesh knew, and kept it in a query.** The fault was in `status`, `plans` or a node's record,
|
||||||
|
for whoever asked. Examples: a rollout waiting for ever (230), a machine refusing every declaration
|
||||||
|
(233), a setting that cannot work stored and stopping the node (096).
|
||||||
|
- **The fault was in a log nothing reads.** Examples: the bus refusing the controller's publishes
|
||||||
|
(187), a dropped report (187, 230).
|
||||||
|
- **The fault was invisible to the mesh itself.** Examples: a resolver outside the mesh closed by its
|
||||||
|
filter (198), a port narrowed without saying (086).
|
||||||
|
|
||||||
|
Only the first group is a matter of telling: the fact exists, and only delivery is missing. The
|
||||||
|
other two need a source first. This effort is about the first, and about giving the other two a
|
||||||
|
place to say something once they can.
|
||||||
|
|
||||||
|
## What the controller computes and does not say
|
||||||
|
|
||||||
|
Read from `status` and `node show` on the day this effort opened. Each line is a fact the controller
|
||||||
|
already holds:
|
||||||
|
|
||||||
|
| fact | where it is today | example that day |
|
||||||
|
|---|---|---|
|
||||||
|
| a machine is out of touch | `node show`: "last heard from — out of touch 10m" | a workstation after an upgrade |
|
||||||
|
| a machine refused its declaration | `status`: "refused" with the reason | the same workstation, for 90 minutes |
|
||||||
|
| a failure repeats and will not fix itself | `status`: "stuck: the same failure N times since …" | 13 times on the laptop |
|
||||||
|
| machines run different hosts | `status`: the version table | after a host release |
|
||||||
|
| something runs that the mesh did not write | `node show`: strays | 16 containers on one machine |
|
||||||
|
| a filter rule the mesh did not write | `status` | one machine |
|
||||||
|
| a plan is waiting | `plans` | issue 230: "for 0s", for ever |
|
||||||
|
| an assignment does not compose | the `assign` answer only | issue 235 |
|
||||||
|
|
||||||
|
None of these is published. The bus carries a seat's own events (a build's outcome), a module's
|
||||||
|
declared events, tool calls and declarations. It carries no event for any line above.
|
||||||
|
|
||||||
|
## What exists to deliver with
|
||||||
|
|
||||||
|
- **A machine's desktop:** the `node-notifier` seat (ADR 0208), held on the laptop. Its `send` verb
|
||||||
|
shows a notification, and `history` lists them. It was used through the console the day this effort
|
||||||
|
opened.
|
||||||
|
- **Mail:** a mail module provides `smtp` to the mesh.
|
||||||
|
- **Chat:** a Matrix server runs as a module on the home server.
|
||||||
|
- **Home automation:** a home-automation module runs there too, and its phone app can receive pushes.
|
||||||
|
- **A seat shape for exactly this:** to-be 32 §5 uses `telegram-sender` (`accepts: send`,
|
||||||
|
`retain 7d`, `emits: delivered, failed`, `serves: status`) as its worked example. A seat's stream
|
||||||
|
exists from registration, so work queues until a holder appears.
|
||||||
|
|
||||||
|
No module sends to Telegram, a phone push service or SMS today.
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# 02 — The channels
|
||||||
|
|
||||||
|
Each channel is a candidate module that delivers what the output seat hands it. They are weighed on
|
||||||
|
the same questions:
|
||||||
|
|
||||||
|
- **Reach:** does it reach the operator away from the machines (phone), or only at a desk?
|
||||||
|
- **Off-mesh:** does it still work when the mesh's own parts (the bus, the controller, the control
|
||||||
|
node's network) are what failed?
|
||||||
|
- **Two-way:** can the operator answer through it: acknowledge, silence, ask?
|
||||||
|
- **Where the words go:** does the message leave the operator's own machines, and to whom?
|
||||||
|
- **What it costs to hold:** a secret, a server, an account, money.
|
||||||
|
|
||||||
|
## The candidates
|
||||||
|
|
||||||
|
### Telegram (required by the operator)
|
||||||
|
|
||||||
|
A bot created with Telegram's bot service sends to one chat: the operator's own, or a group.
|
||||||
|
|
||||||
|
- **Reach:** the phone and every desktop, with push.
|
||||||
|
- **Off-mesh:** sending needs only outbound HTTPS from any machine. No inbound port, no server of the
|
||||||
|
mesh's own. A second machine can hold the same bot token and send when the first is the one that
|
||||||
|
failed.
|
||||||
|
- **Two-way:** yes. Inline buttons on a message (acknowledge, silence for an hour) and commands to
|
||||||
|
the bot, read by long polling over outbound HTTPS. This makes Telegram the strongest candidate for
|
||||||
|
answering, and the riskiest (see [03](03-open-questions.md), Q7).
|
||||||
|
- **Where the words go:** to Telegram's servers. Bot chats are not end-to-end encrypted. What a
|
||||||
|
message may contain is therefore a rule this effort must set.
|
||||||
|
- **Cost:** one secret (the bot token) and the chat's id. Free. Rate limits are far above what an
|
||||||
|
operator should receive.
|
||||||
|
- **Formatting:** short text with a little markup, buttons and links. Enough for a subject, a
|
||||||
|
machine role, a severity and one line of why.
|
||||||
|
|
||||||
|
### The desktop notifier (exists)
|
||||||
|
|
||||||
|
The `node-notifier` seat's `send` verb on the machine the operator is at.
|
||||||
|
|
||||||
|
- **Reach:** only at that machine, only while a session is up.
|
||||||
|
- **Off-mesh:** no. It is reached through the mesh's tools.
|
||||||
|
- **Two-way:** dunst has actions, which a click can answer, but nothing reads them back yet.
|
||||||
|
- **Where the words go:** nowhere; it is local.
|
||||||
|
- **Cost:** none.
|
||||||
|
- **Its place:** the gentlest channel, for a warning while the operator is at a desk. "At a desk" is
|
||||||
|
itself a question: an unlocked session on a machine with recent input.
|
||||||
|
|
||||||
|
### ntfy (or Gotify): a self-hosted phone push
|
||||||
|
|
||||||
|
A small server publishes topics; its phone app subscribes.
|
||||||
|
|
||||||
|
- **Reach:** the phone, with push.
|
||||||
|
- **Off-mesh:** only if the server runs outside what failed. On the control node it fails with it.
|
||||||
|
- **Two-way:** action buttons can call a URL, which is an inbound path to design.
|
||||||
|
- **Where the words go:** stays on the operator's machines when self-hosted. ntfy's iOS push passes
|
||||||
|
through an upstream relay unless configured otherwise.
|
||||||
|
- **Cost:** a module with a container and a routed name; a token per topic.
|
||||||
|
|
||||||
|
### Matrix (a server exists as a module)
|
||||||
|
|
||||||
|
A bot account posts to a room the operator is in.
|
||||||
|
|
||||||
|
- **Reach:** phone and desktop through any Matrix client.
|
||||||
|
- **Off-mesh:** no, the server is one of the mesh's modules.
|
||||||
|
- **Two-way:** yes, by messages to the bot.
|
||||||
|
- **Where the words go:** stays on the operator's server, end-to-end encrypted if the bot supports it.
|
||||||
|
- **Cost:** a bot account, a secret.
|
||||||
|
|
||||||
|
### Mail (a mail module provides `smtp`)
|
||||||
|
|
||||||
|
- **Reach:** everywhere, without urgency.
|
||||||
|
- **Off-mesh:** no, if the mesh's own mail server sends. Yes, through an outside relay.
|
||||||
|
- **Two-way:** no, not usefully.
|
||||||
|
- **Its place:** the record and the digest: a daily summary of what was said and resolved, and the
|
||||||
|
fallback when nothing else acknowledged.
|
||||||
|
|
||||||
|
### The home-automation companion app (a module exists)
|
||||||
|
|
||||||
|
Its phone app takes pushes and actionable notifications, and the home has lights and speakers.
|
||||||
|
|
||||||
|
- **Reach:** the phone, and the house itself: a light that turns a colour.
|
||||||
|
- **Off-mesh:** no, the home server is a node.
|
||||||
|
- **Its place:** a playful critical channel, not a primary one.
|
||||||
|
|
||||||
|
### The bar on the desktop
|
||||||
|
|
||||||
|
An `i3status-rust` block showing the count of open messages, red while one is critical.
|
||||||
|
|
||||||
|
- **Reach:** the desk only, and silent.
|
||||||
|
- **Its place:** the ambient state. Nothing interrupts the operator, and they always see whether
|
||||||
|
something is open.
|
||||||
|
|
||||||
|
### The console (an agent session)
|
||||||
|
|
||||||
|
A message the next agent session opens with ("two things happened while you were away").
|
||||||
|
|
||||||
|
- **Its place:** context for the agent working on the mesh rather than an alert. It falls out of the
|
||||||
|
message store if the store is queryable.
|
||||||
|
|
||||||
|
### Others, noted and not pursued now
|
||||||
|
|
||||||
|
- **SMS or a voice call** through a paid gateway. It is the only channel that works with no data
|
||||||
|
connection, and the only one that costs per message.
|
||||||
|
- **Signal**, through an unofficial client: no bot API, and a registered number.
|
||||||
|
- **Discord or Slack** webhooks: the words go to a third party, as with Telegram, without its two-way
|
||||||
|
strength.
|
||||||
|
- **Pushover:** paid, closed, and a phone push service much like ntfy.
|
||||||
|
- **An external dead-man service** (a heartbeat URL that alerts when pings stop). It belongs to
|
||||||
|
[03](03-open-questions.md), Q6, as the watcher's watcher rather than as a channel.
|
||||||
|
|
||||||
|
## A first reading
|
||||||
|
|
||||||
|
- **Telegram** is the primary phone channel, and the only candidate that is cheap, off-mesh capable
|
||||||
|
and two-way at once.
|
||||||
|
- **The desktop notifier** is for the desk.
|
||||||
|
- **The bar** shows the ambient state.
|
||||||
|
- **Mail** carries the digest and the record.
|
||||||
|
- **ntfy and Matrix** are self-hosted alternatives for an operator who keeps words off third parties.
|
||||||
|
The seat must make that a choice, not a rewrite.
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# 03 — Open questions
|
||||||
|
|
||||||
|
Each question names the options seen so far. None is decided here.
|
||||||
|
|
||||||
|
## Q1. The seat
|
||||||
|
|
||||||
|
**What speaks for the mesh to its operator?**
|
||||||
|
|
||||||
|
- **a.** One seat in the mesh's own set, held once for the mesh. Working name: `operator-channel`.
|
||||||
|
- It **accepts** `notify` (a work queue, as to-be 32 §5 designs `telegram-sender`), so a message
|
||||||
|
waits until a holder appears.
|
||||||
|
- It **emits** `delivered`, `acknowledged` and `resolved`.
|
||||||
|
- It **serves** `open` (what is unresolved now) and `history`.
|
||||||
|
- **b.** No seat: every source calls every channel. Rejected in advance, because each source would
|
||||||
|
learn every channel. This is the inversion ADR 0126 exists to prevent.
|
||||||
|
- **c.** Each channel as its own seat, with routing in the sources. Same objection as b, one level up.
|
||||||
|
|
||||||
|
Under a, the holder routes. The channels are modules that **contribute** themselves to the seat
|
||||||
|
(ADR 0210): a channel extends the seat, and so depends on it. Whether the holder is a module of its
|
||||||
|
own or part of the controller is open. A module keeps the controller small. The controller already
|
||||||
|
holds most of the facts.
|
||||||
|
|
||||||
|
## Q2. What a message is
|
||||||
|
|
||||||
|
The first shape seen: a **subject** (what it is about: a machine's role, a module, a plan), a
|
||||||
|
**kind** (out of touch, refused, stuck, late, …), a **severity**, a one-line **why**, a link to
|
||||||
|
the tool that shows more, and a **key** that makes it the same message the next time it is said.
|
||||||
|
|
||||||
|
- **Severity:** two levels (needs you now / when you can), or three (critical / warning / info)?
|
||||||
|
Every extra level is a routing rule somebody must keep right.
|
||||||
|
- **The key** is what makes deduplication possible. "Machine X out of touch" said every minute is
|
||||||
|
one message, still open, not sixty.
|
||||||
|
|
||||||
|
## Q3. The life of a message
|
||||||
|
|
||||||
|
open → (acknowledged) → resolved.
|
||||||
|
|
||||||
|
- **Deduplicate** by key while open.
|
||||||
|
- **Resolve** when the source stops saying it, or says it is over ("back in touch after 14 min"). A
|
||||||
|
channel that can edit its message (Telegram can) updates it in place rather than sending a second.
|
||||||
|
- **Acknowledge** from any channel that can answer, which stops escalation and repeats.
|
||||||
|
- **Repeat or escalate** an unacknowledged critical message after a while, to the next channel.
|
||||||
|
- **Where the open set lives:** the seat's own state, in a key-value bucket (to-be 32's `state:`), so
|
||||||
|
`open` answers after a restart.
|
||||||
|
|
||||||
|
## Q4. Routing and presence
|
||||||
|
|
||||||
|
- **By severity:** critical goes to every channel at once. A warning goes to the desk when the
|
||||||
|
operator is at one, otherwise to the phone, otherwise to the digest.
|
||||||
|
- **Presence:** "at a desk" needs a fact the mesh does not hold yet. Candidates: an unlocked
|
||||||
|
graphical session with recent input, read from the `node-lock-screen` and `node-login-manager`
|
||||||
|
seats' holders. Nothing more invasive.
|
||||||
|
- **Quiet hours:** a setting of the seat's holder (ADR 0174). Critical overrides it, or not, as the
|
||||||
|
operator chooses.
|
||||||
|
- **Rate:** a cap per hour per channel, with the excess folded into one summary, so a storm (a
|
||||||
|
network outage where every node is out of touch) arrives as one message naming many.
|
||||||
|
|
||||||
|
## Q5. The sources
|
||||||
|
|
||||||
|
The first sources are the facts in [01](01-what-the-mesh-already-knows.md), all in the controller
|
||||||
|
today:
|
||||||
|
|
||||||
|
- a machine out of touch;
|
||||||
|
- a declaration refused;
|
||||||
|
- a stuck failure;
|
||||||
|
- a plan late (once issue 230 gives a wait an age);
|
||||||
|
- an assignment that does not compose (issue 235);
|
||||||
|
- a host version split.
|
||||||
|
|
||||||
|
**How each becomes an event:**
|
||||||
|
- **a.** The controller emits an event per change of state, and the seat's holder consumes them.
|
||||||
|
- **b.** The controller calls `notify` itself.
|
||||||
|
|
||||||
|
With a, the controller learns nothing about telling: other consumers (a board, a log) get the same
|
||||||
|
facts, and the holder decides what is worth a message. With b, the controller decides severity.
|
||||||
|
|
||||||
|
**Modules as sources:** a module may `use` the seat to tell the operator something of its own
|
||||||
|
(a backup failed, a certificate is close to expiry), with the same message shape.
|
||||||
|
|
||||||
|
## Q6. The watcher's watcher
|
||||||
|
|
||||||
|
When the controller, the bus or the control node is what failed, nothing above runs. Options:
|
||||||
|
|
||||||
|
- **A dead-man signal:** the seat's holder sends a heartbeat out of the mesh (a ping to an external
|
||||||
|
heartbeat service), which alerts the operator by its own means when pings stop.
|
||||||
|
- **A second holder of the Telegram channel on another machine** that sends directly, without the
|
||||||
|
bus, when it stops hearing the controller for longer than a bound.
|
||||||
|
- **Each host** sending a last message itself when it loses the mesh for longer than a bound. This
|
||||||
|
needs the channel's secret on every machine, a cost to weigh.
|
||||||
|
|
||||||
|
The first is the cheapest and the only one that also covers "the whole house is offline".
|
||||||
|
|
||||||
|
## Q7. Answering back
|
||||||
|
|
||||||
|
Telegram, and Matrix, can carry the operator's answers.
|
||||||
|
|
||||||
|
- **Acknowledge and silence** are safe: they change only the message's state.
|
||||||
|
- **Commands** ("push the workstation", "show status") turn a chat account into a door to the
|
||||||
|
controller. If it ever comes, it needs:
|
||||||
|
- its own record;
|
||||||
|
- a narrow verb set;
|
||||||
|
- a check that the answer came from the operator's own account and chat;
|
||||||
|
- and probably a confirmation step.
|
||||||
|
|
||||||
|
The first version should probably answer with acknowledge and silence only.
|
||||||
|
|
||||||
|
## Q8. What may leave the mesh
|
||||||
|
|
||||||
|
Telegram, and any third-party channel, carries the words to someone else's servers. A message
|
||||||
|
names a machine, a module and a reason, which is operational detail.
|
||||||
|
|
||||||
|
- **What may a message contain?** Roles rather than addresses; no secrets, tokens or paths; a reason
|
||||||
|
in words. The rule must be enforced by the seat's holder, not hoped for from each source.
|
||||||
|
- **Is a self-hosted channel required for anything above a severity?**
|
||||||
|
- **The bot token and chat id** are secrets of the channel's module, delivered as any module secret is.
|
||||||
|
|
||||||
|
## Q9. How it is checked
|
||||||
|
|
||||||
|
A rule this effort produces must say how it is verified. Candidates:
|
||||||
|
|
||||||
|
- a message said twice with one key is one message;
|
||||||
|
- a resolved source resolves its message;
|
||||||
|
- a critical message reaches every channel within a bound;
|
||||||
|
- a message containing an address or a secret is refused;
|
||||||
|
- the dead-man signal fires when the holder is stopped.
|
||||||
|
|
||||||
|
Each is a test of the holder, or a live drill: stop a machine's host and time the message.
|
||||||
+5
@@ -93,6 +93,11 @@ A node with contributions and no holder writes them nowhere. The holder's absenc
|
|||||||
node's assignments, and no contributor is refused for it, because a missing `PATH` entry is a gap, not
|
node's assignments, and no contributor is refused for it, because a missing `PATH` entry is a gap, not
|
||||||
a broken machine.
|
a broken machine.
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-04, by [ADR 0210](0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md).** A contribution is now a dependency on
|
||||||
|
> node-environment, met and refused as ADR 0207 says, so a node without the holder refuses the
|
||||||
|
> contributor instead of writing the contribution nowhere. The rest of this section, and the
|
||||||
|
> decision, stand.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- A shell's part in the environment is one line in its always-read startup file, sourcing the
|
- A shell's part in the environment is one line in its always-read startup file, sourcing the
|
||||||
|
|||||||
@@ -87,6 +87,12 @@ reports, `graphical-session`, still gates the display server itself.
|
|||||||
The holder of `node-display-server` places them with `${shell:xinitrc:<slot>}` and
|
The holder of `node-display-server` places them with `${shell:xinitrc:<slot>}` and
|
||||||
`${shell:xresources:<slot>}`.
|
`${shell:xresources:<slot>}`.
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-04, by [ADR 0210](0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md).** A contributor no longer places its own
|
||||||
|
> file in another tool's directory. It contributes to the tool's seat, and the seat's holder places it,
|
||||||
|
> in that directory or through a placeholder. Every contribution, the `xinitrc` and `xresources` slots
|
||||||
|
> included, is a dependency on the seat that receives it. What a contribution contains still follows
|
||||||
|
> the tool's grain.
|
||||||
|
|
||||||
**5. The display server's module writes the session's start.** It writes a block at the start of
|
**5. The display server's module writes the session's start.** It writes a block at the start of
|
||||||
`~/.xinitrc`, in this order:
|
`~/.xinitrc`, in this order:
|
||||||
|
|
||||||
|
|||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-04
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 209. A login on a node moves that node to the account it logged in to; an API key is added from any node, sealed
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0206](0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md)
|
||||||
|
made a licence an account the manager learns from what the nodes report, adopted by refreshing it, and
|
||||||
|
bound a node to a licence automatically only when it was bound to nothing (§7); every later change was a
|
||||||
|
person's act through `bind` and `switch`. It went live on 2026-10-04 with one account, bound to all four
|
||||||
|
nodes.
|
||||||
|
|
||||||
|
**The operator then asked what happens on a login to a second account on one node, and traced, the
|
||||||
|
answer was wrong.** The manager adopts the second account as a new licence — and leaves the node bound to
|
||||||
|
the first. The node is left holding the second account's access token, a refresh token the adoption just
|
||||||
|
spent, and a binding to the first; at the first account's next rotation it is handed a token its own state
|
||||||
|
file does not name. A login is the most direct thing a person does on a machine about which account it
|
||||||
|
uses, and the mesh read it as a contribution of a grant only.
|
||||||
|
|
||||||
|
**An API key could enter only from a file on the manager's node** (ADR 0206, design 39 §6), so adding one
|
||||||
|
meant reaching that machine. The operator asked for a streamlined process for both.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **A login on a node switches that node to the account logged in to.** Chosen.
|
||||||
|
2. **Keep ADR 0206 §7, and have the person `switch` after logging in.** Rejected: the step is easy to
|
||||||
|
forget and the state between the login and the switch is the broken one described above.
|
||||||
|
3. **Refuse to adopt a login for an account other than the node's binding.** Rejected: it discards what
|
||||||
|
the person plainly meant, and a second account could then enter only by a separate act.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. A login on a node is that node's choice of account.** When the manager adopts a node's login (ADR
|
||||||
|
0206 §4) — a new account, or a newer login of one it holds — it binds that node to the licence the login
|
||||||
|
belongs to. If the node was bound to another licence, this is a switch: the node is handed the new
|
||||||
|
licence's access token and its agent's account is pointed at it, as `switch` does. Every other node stays
|
||||||
|
where it is. `bind`, `switch` and `release` remain for moving a node without a login.
|
||||||
|
|
||||||
|
**2. A login that does not refresh moves nothing.** The candidate is recorded dead (ADR 0206 §4) and the
|
||||||
|
node keeps its binding; the person logs in again.
|
||||||
|
|
||||||
|
**3. An API key is added from any node, sealed, never as an argument.** The agent module serves a tool
|
||||||
|
that reads a key from a file on its own node, seals it to the manager's public key — which the seat now
|
||||||
|
serves as a verb — and hands it to the seat's `adopt` on request/reply; the file is removed once the
|
||||||
|
manager has taken it. Optionally the same call binds that node to the new licence. The seat's `adopt`
|
||||||
|
still also takes a file on the manager's node. An API key is a licence of its own, never an account's:
|
||||||
|
nothing is learned about it from a report, and it moves a node only when a person says so.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Logging in on a node is the whole of moving that node to an account, new or known. The mesh's state
|
||||||
|
stays consistent: the binding, the token on the node and the account its agent names agree.
|
||||||
|
- A second account enters the mesh by one login, and only the node it was logged in on uses it.
|
||||||
|
- **What got harder:** a person who logs in on a node to try an account moves that node; moving it back is
|
||||||
|
`switch`. Said in the seat's own description of `switch`, and in the agent module's instruction file.
|
||||||
|
- The key file on a node exists only until the manager has taken it; the key then lives encrypted in the
|
||||||
|
manager's store alone (ADR 0183).
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A login for another account moves its node and no other | the manager's test: two accounts, the login on one node adopted, that node switched, the others unchanged |
|
||||||
|
| A newer login of a known account on a node bound elsewhere moves that node | the manager's test |
|
||||||
|
| A login that does not refresh moves nothing | the manager's test: the binding unchanged, the candidate dead |
|
||||||
|
| An API key never crosses the bus in the clear and its file is gone afterwards | the agent module's test: the request carries a sealed box only; the file is removed after the seat answered |
|
||||||
|
| Live | a login to a second account on one workstation: a second licence appears, that workstation is bound to it and its agent names it, the other nodes keep the first |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0206](0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md) — the flow this extends; §7 is changed by decision 1
|
||||||
|
- [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
|
||||||
|
- [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)
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-04
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 210. A tool's configuration is its seat holder's, and every other module extends it through the seat
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
One fault kept coming back while the machines' modules were rolled out
|
||||||
|
([to-be 42](../03-DESIGN/01-to-be/42-the-machines-modules-in-order.md)): two modules want the same
|
||||||
|
thing on a node.
|
||||||
|
|
||||||
|
- The bar module and the package manager's module both declared the package that brings the
|
||||||
|
package manager's helper scripts. The node stopped resolving
|
||||||
|
([issue 235](../04-ISSUES/235-an-assignment-that-cannot-be-composed-is-recorded-anyway/00-report.md)).
|
||||||
|
- The launcher, the clipboard manager, the wallpaper and the bar each wrote a file of their own into
|
||||||
|
the window manager's include directory, as [ADR 0208](0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md)
|
||||||
|
§4 allowed. Nothing says those modules need the window manager. Assigned without it, they write
|
||||||
|
configuration nothing reads. Assigned with a different session holder, they write into a directory
|
||||||
|
that holder does not own.
|
||||||
|
- [ADR 0203](0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md) §5
|
||||||
|
lets a module contribute to the environment on a node that has no holder: the contribution is
|
||||||
|
written nowhere, and nothing says so.
|
||||||
|
|
||||||
|
The mesh already has the piece that answers this.
|
||||||
|
[ADR 0207](0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md) made a module depend on
|
||||||
|
the seat that applies its resources, derived from what it declares. A contribution is the same kind
|
||||||
|
of need. It is configuration that only the seat's holder can apply.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Let the first module to declare a file or package own it,** and refuse the second. Rejected:
|
||||||
|
ownership then depends on the order modules were written, and the second module has no lawful way
|
||||||
|
to say what it needs.
|
||||||
|
2. **Allow shared declarations** of one package or file by several modules, merged by the host.
|
||||||
|
Rejected: a shared file has no owner to answer for it, and removing one module cannot tell what it
|
||||||
|
alone put there.
|
||||||
|
3. **Each tool's configuration belongs to the module holding the tool's seat. Every other module
|
||||||
|
extends it with a contribution to that seat, and a contribution is a dependency on the seat.**
|
||||||
|
Chosen. It is the operator's statement of the rule.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**1. One owner.** A tool's configuration files, and the tool's package, belong to the module that
|
||||||
|
holds the tool's seat on the node. Only that module writes them.
|
||||||
|
- The package manager's configuration and helper packages are the package manager's module's.
|
||||||
|
- The account's environment is node-environment's holder's.
|
||||||
|
- The window manager's configuration is node-display-session's holder's.
|
||||||
|
|
||||||
|
**2. Other modules extend, never write.** A module that needs something in another tool's
|
||||||
|
configuration declares a **contribution to that tool's seat**:
|
||||||
|
- its content, in the grain the seat defines (variables and paths, shell code for a named shell and
|
||||||
|
slot, a window-manager configuration fragment, a notifier rule);
|
||||||
|
- never a path inside the holder's files or directories.
|
||||||
|
|
||||||
|
The holder places what it receives:
|
||||||
|
- the controller renders the contributions into the holder's files through the holder's placeholders,
|
||||||
|
as [ADR 0203](0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md) and
|
||||||
|
[ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md) already do;
|
||||||
|
- or the holder writes each contribution to its tool's own drop-in directory. That directory is then
|
||||||
|
the holder's resource, not the contributor's.
|
||||||
|
|
||||||
|
**3. A contribution is a dependency on the seat that receives it.** The controller derives it from the
|
||||||
|
contribution, as ADR 0207 derives one from a resource. It is met, checked and refused exactly as ADR
|
||||||
|
0207 §3 and §4 say:
|
||||||
|
- refused at `assign` when no module on the node holds the seat and the catalogue has a holder;
|
||||||
|
- refused at composition after the switch.
|
||||||
|
|
||||||
|
**4. Needing what another module's package delivers is the same.** A module that needs a program
|
||||||
|
another seat's holder installs does not declare that package. It depends on the seat, and through
|
||||||
|
the seat's verbs where they exist. One package is declared by one module on a node.
|
||||||
|
|
||||||
|
**5. A seat says what it receives.** A seat lists the contribution kinds its holder accepts. A
|
||||||
|
contribution of a kind the seat does not list is refused at registration.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **ADR 0203 §5** no longer holds: a module contributing to the environment depends on
|
||||||
|
node-environment, and a node without the holder refuses it. The rest of that record stands.
|
||||||
|
- **ADR 0208 §4**: its first list, contributors placing their own files in another tool's directory,
|
||||||
|
is replaced by §2 above. The tool's grain stays the guide for what a contribution contains. The
|
||||||
|
`xinitrc` and `xresources` slots already work this way, and now carry a dependency on
|
||||||
|
node-display-server.
|
||||||
|
- **The desktop modules change:** the launcher, the clipboard manager, the wallpaper and the bar
|
||||||
|
contribute their window-manager lines to node-display-session instead of writing into the include
|
||||||
|
directory. The bar keeps relying on the package manager's helper scripts through node-package-manager.
|
||||||
|
- **The two kinds of collision cannot recur:**
|
||||||
|
- two modules declaring one package or one file;
|
||||||
|
- a contribution to a seat nobody on the node holds.
|
||||||
|
A composition that finds either is a fault in a module, not a state a node can be left in.
|
||||||
|
- **What got harder:** a seat that receives contributions must define their grain, and its holder must
|
||||||
|
place them. Each new kind is a small change to the controller's renderer or to the holder.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A contribution derives a dependency on the seat that receives it | the controller's resolve tests |
|
||||||
|
| A contribution to a seat with no holder on the node is refused at `assign` when the catalogue has a holder | the same tests, and `assign` live |
|
||||||
|
| One package or file is declared by one module on a node | the controller's composition test, and `module check` across the catalogue |
|
||||||
|
| A contribution of a kind its seat does not list is refused | the catalogue check, which registration runs |
|
||||||
|
| No module declares a path inside another module's files or directories | the catalogue check |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [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),
|
||||||
|
[ADR 0208](0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md)
|
||||||
|
- [Issue 235](../04-ISSUES/235-an-assignment-that-cannot-be-composed-is-recorded-anyway/00-report.md)
|
||||||
@@ -192,6 +192,7 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **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)
|
- **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)
|
||||||
- **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)
|
- **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)
|
- **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)
|
||||||
|
- **0210** — [A tool's configuration is its seat holder's, and every other module extends it through the seat](0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md)
|
||||||
|
|
||||||
### Its tiers, from the bottom up
|
### Its tiers, from the bottom up
|
||||||
|
|
||||||
@@ -307,6 +308,7 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **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)
|
- **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)
|
- **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)
|
- **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)
|
||||||
|
- **0209** — [A login on a node moves that node to the account it logged in to; an API key is added from any node, sealed](0209-a-login-on-a-node-moves-that-node-to-its-account-and-an-api-key-is-added-from-any-node-sealed.md)
|
||||||
|
|
||||||
### How it is built
|
### How it is built
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ status: designed
|
|||||||
code: []
|
code: []
|
||||||
updated: 2026-10-04
|
updated: 2026-10-04
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0209-a-login-on-a-node-moves-that-node-to-its-account-and-an-api-key-is-added-from-any-node-sealed.md
|
||||||
- 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md
|
- 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/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/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
|
||||||
@@ -159,6 +160,11 @@ decides it and [ADR 0206](../../02-DECISIONS/0206-a-node-reports-the-anthropic-g
|
|||||||
the agent here never refreshes, and a refresh token appearing later is a person's login, reported like
|
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
|
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;
|
prints the key from the module's state, so no file under the home is touched;
|
||||||
|
- **adds an API key from this node** (*ADR 0209*): `claude_code_add_api_key` reads the key from a file
|
||||||
|
here, seals it to the manager's `public-key`, hands it to the seat's `adopt`, removes the file once
|
||||||
|
taken, and on request switches this node to the new licence;
|
||||||
|
- **follows a login made here**: a login to another account is adopted and moves this node to it (ADR
|
||||||
|
0209) — nothing for this module to do beyond reporting it;
|
||||||
- **serves `claude_code_status`**: which licence and kind this node holds, when the token expires, whether
|
- **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.
|
the file matches what was handed over — by fingerprint, never by value.
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ status: designed
|
|||||||
code: []
|
code: []
|
||||||
updated: 2026-10-04
|
updated: 2026-10-04
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0209-a-login-on-a-node-moves-that-node-to-its-account-and-an-api-key-is-added-from-any-node-sealed.md
|
||||||
- 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md
|
- 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/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/0024-model-access-is-a-provision.md
|
||||||
@@ -138,12 +139,16 @@ report, and adopted by refreshing it.
|
|||||||
- **The latest login wins.** A bound node is handed an access token only and its file holds no refresh
|
- **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,
|
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.
|
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
|
- **A login moves its node** (*amended 2026-10-04 by [ADR 0209](../../02-DECISIONS/0209-a-login-on-a-node-moves-that-node-to-its-account-and-an-api-key-is-added-from-any-node-sealed.md)*): the node a login was
|
||||||
is bound to it. Every later change is `bind`, `switch` or `release`.
|
adopted from is bound to that login's licence — switched, if it was bound to another — and every node
|
||||||
|
bound to nothing whose report names an account the manager holds is bound to it. `bind`, `switch` and
|
||||||
|
`release` move a node without a login.
|
||||||
- **The identity guard** files a grant under the identity the node read; where the vendor's refresh
|
- **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.
|
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
|
- **An API key** enters from any node (*ADR 0209*): the agent module there reads it from a file on its own
|
||||||
on the manager's node, never as an argument.
|
node, seals it to the manager's key (the seat's `public-key` verb) and hands it to `adopt`, removing the
|
||||||
|
file once taken — or `adopt` reads a file on the manager's node. Never an argument, never on a stream.
|
||||||
|
An API key is a licence of its own and moves a node only through `bind` or `switch`.
|
||||||
|
|
||||||
## 7. What it emits and serves
|
## 7. What it emits and serves
|
||||||
|
|
||||||
@@ -155,7 +160,8 @@ gone; a rotation or a switch is a new generation in the `bindings` state.
|
|||||||
|
|
||||||
**The seat's verbs**, the contract every future holder must serve: `licences` (each with kind,
|
**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
|
identity, expiry, failures, who is bound), `bindings`, `bind`, `switch`, `release`, `refresh` (now, one
|
||||||
or all), `usage` (current and history), `adopt`, and `current` (a consumer's token, sealed to the key the
|
or all), `usage` (current and history), `adopt` (a file on the manager's node, or a key sealed to its
|
||||||
|
`public-key` — ADR 0209), `public-key`, 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.
|
consumer sends — ADR 0206). The manager asks a node for a candidate grant by the agent module's own tool.
|
||||||
|
|
||||||
## 8. Settings
|
## 8. Settings
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ decisions:
|
|||||||
- 02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.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/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
|
- 02-DECISIONS/0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md
|
||||||
|
- 02-DECISIONS/0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 42. The machines' modules, in order
|
# 42. The machines' modules, in order
|
||||||
@@ -93,6 +94,13 @@ In order:
|
|||||||
|
|
||||||
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).
|
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).
|
||||||
|
|
||||||
|
**Who writes what** is [ADR 0210](../../02-DECISIONS/0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md): a tool's configuration belongs to the
|
||||||
|
holder of its seat. The launcher, the clipboard manager, the wallpaper and the bar contribute their
|
||||||
|
window-manager lines to `node-display-session`, and the window manager's module places them. They do
|
||||||
|
not write into its include directory. Each contribution is a dependency on the seat that receives it,
|
||||||
|
so assigning one of them without a window manager is refused. The first versions, which still write
|
||||||
|
the include files themselves, move to contributions once the controller derives the dependency.
|
||||||
|
|
||||||
## Phase 3 — one machine model
|
## Phase 3 — one machine model
|
||||||
|
|
||||||
The laptop's hardware module (vendor daemon, GPU mode, charge limit, logind, brightness and vendor keys)
|
The laptop's hardware module (vendor daemon, GPU mode, charge limit, logind, brightness and vendor keys)
|
||||||
|
|||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-10-04
|
||||||
|
located-in: []
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 233 — A host without its package manager's configuration refuses the declaration that would restore it
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
2026-10-04, on a workstation. The package manager's module writes that tool's main configuration
|
||||||
|
file whole, keeping the file it found. A declaration that no longer named the module
|
||||||
|
([issue 234](../234-a-later-declaration-left-out-four-assigned-modules-and-a-machine-applied-it/00-report.md))
|
||||||
|
reached a host older than the fix that gives a written-over file back its original. The host
|
||||||
|
removed the file, and the kept original stayed where the host keeps such files.
|
||||||
|
|
||||||
|
From the next declaration on, the host refused every declaration whole:
|
||||||
|
|
||||||
|
```
|
||||||
|
refused a declaration: this is the arch host and pacman does not answer here. Either this machine
|
||||||
|
is not Arch, or its package database is broken: pacman exited 1: error: config file … could not be
|
||||||
|
read: No such file or directory
|
||||||
|
```
|
||||||
|
|
||||||
|
The machine stayed in that state for an hour and a half. It retried every five minutes and was
|
||||||
|
refused every time. Two declarations it refused would have repaired it:
|
||||||
|
|
||||||
|
- the current one, which names the package manager's module again and so writes the file;
|
||||||
|
- the one carrying the newer host, which gives a kept original back.
|
||||||
|
|
||||||
|
Neither could be applied. A person restored the kept original by hand, and the next push applied.
|
||||||
|
|
||||||
|
`status` showed the machine as `refused` with the error above. That is correct, but nothing said
|
||||||
|
that the mesh's own tools could no longer reach it.
|
||||||
|
|
||||||
|
## Why it matters beyond this instance
|
||||||
|
|
||||||
|
The host checks that the package manager answers before it applies anything. That check is right for
|
||||||
|
a machine that is not what the mesh thinks it is. Here the check depends on a file that the mesh's own
|
||||||
|
modules own and can remove. Once that file is gone:
|
||||||
|
|
||||||
|
- every module's change waits behind it, including the host's own upgrade;
|
||||||
|
- the only way back is a person on the machine;
|
||||||
|
- so a fault that one declaration caused, and a later declaration would fix, cannot be undone
|
||||||
|
through the mesh.
|
||||||
|
|
||||||
|
The same shape holds for anything the host probes before applying. If what it probes is one of
|
||||||
|
the mesh's own resources, one bad declaration can wedge the machine.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
1. Should a probe that fails refuse the declaration whole? Or should it fail only the resources that
|
||||||
|
need the tool, and apply the rest (files, units, the host's own upgrade)? The rest may include
|
||||||
|
the very resource that restores the tool.
|
||||||
|
2. Should the host refuse to remove a file a seat's holder needs to answer, or warn before it does?
|
||||||
|
3. Is a machine that refuses every declaration for longer than one apply a fault the mesh raises by
|
||||||
|
itself ([issue 187](../187-the-mesh-tells-nobody-when-it-stops-working/00-report.md))? Today it
|
||||||
|
appears only to someone who asks for `status`.
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-10-04
|
||||||
|
located-in: []
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 234 — A later declaration left out four assigned modules, and a machine applied it
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
2026-10-04, on a workstation. Four modules (the package manager's, sudo, localization and the
|
||||||
|
container runtime's) had been assigned and pushed, and the host was applying them in one long apply,
|
||||||
|
about eleven minutes on a loaded machine. Seven more declarations arrived while it worked. When the
|
||||||
|
apply ended, the host logged six times `set aside a declaration: a newer one arrived with it` and
|
||||||
|
applied the one it kept.
|
||||||
|
|
||||||
|
The host chooses by the sequence a declaration carries
|
||||||
|
([issue 107](../107-a-declaration-carries-no-order/00-report.md)), so the one it kept was the latest
|
||||||
|
the controller had sent. That declaration did not name the four modules. Over the next eight minutes
|
||||||
|
the host undeclared all of them:
|
||||||
|
|
||||||
|
```
|
||||||
|
removed sudo.operator (…)
|
||||||
|
removed pacman.config (…)
|
||||||
|
forgotten pacman.package (pacman)
|
||||||
|
removed localization.locale (…)
|
||||||
|
removed docker.prune-service (…)
|
||||||
|
```
|
||||||
|
|
||||||
|
Throughout, the controller's assignments named the four modules on that machine, and they still do:
|
||||||
|
`plan` for the machine lists them. Nobody had unassigned them.
|
||||||
|
|
||||||
|
What happened around it:
|
||||||
|
|
||||||
|
- A catalogue asked the controller to catch up twice in the same minutes, and every builder rebuilt
|
||||||
|
the same catalogue commit.
|
||||||
|
- The controller daemon restarted three minutes after the stale declaration was applied.
|
||||||
|
- Another session was working on the mesh and pushing at the same time.
|
||||||
|
|
||||||
|
The controller logs no send. Which process sent the stale declaration, and from what view, cannot be
|
||||||
|
read back from anything the mesh keeps.
|
||||||
|
|
||||||
|
## Why it matters beyond this instance
|
||||||
|
|
||||||
|
A declaration is the mesh's word on what a machine should be, and the host applies it in full,
|
||||||
|
including removing what it does not name. A stale one is not harmless: here it removed four modules,
|
||||||
|
and on a host older than the fix for written-over files it deleted four system files outright
|
||||||
|
([issue 233](../233-a-host-without-its-package-managers-configuration-refuses-the-declaration-that-would-restore-it/00-report.md)).
|
||||||
|
|
||||||
|
[Issue 204](../204-a-controller-handover-re-sent-every-node-a-stale-declaration/00-report.md) was this
|
||||||
|
family once before and is marked resolved. Ordering by sequence (issue 107) protects against an old
|
||||||
|
declaration arriving late. It does not protect against a declaration that is new in sequence but
|
||||||
|
composed from an old view.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
1. Where can a declaration be composed with fewer modules than the assignments hold? Candidates:
|
||||||
|
- a send from a process with an out-of-date view;
|
||||||
|
- composition while a module's build is being replaced;
|
||||||
|
- a plan sending what it composed when it was made.
|
||||||
|
2. Should every send be recorded with its sequence and its sender, so `status` can show what a machine
|
||||||
|
was last told and by whom?
|
||||||
|
3. Should a declaration that removes something carry a check the host can verify? For example, the
|
||||||
|
assignment generation it was composed from, so a host refuses one older than the generation it has
|
||||||
|
already applied.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-10-04
|
||||||
|
located-in: []
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 235 — An assignment that cannot be composed is recorded anyway, and the node is one push from leaving the mesh
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
2026-10-04, assigning thirteen desktop modules to a workstation in one `assign`. The controller
|
||||||
|
answered `ok: false`. Its output first confirmed every module (`<node> is assigned <module>`, thirteen
|
||||||
|
times), then repeated the same reason eleven times:
|
||||||
|
|
||||||
|
```
|
||||||
|
<node> is not counted as on the network: it does not resolve: these assignments cannot be applied:
|
||||||
|
- i3status-rust and pacman both declare the package "pacman-contrib"
|
||||||
|
<node> is left out of the rest of the mesh: it does not resolve: …
|
||||||
|
```
|
||||||
|
|
||||||
|
and ended with the node's needs from the control node (artifact store, internal CA, package registry)
|
||||||
|
reported as unmet "because they are not both on the private network".
|
||||||
|
|
||||||
|
The thirteen assignments stayed recorded. `plan` for the node failed until one module was unassigned
|
||||||
|
by hand. Had anything pushed in that window — a person, a plan rolling out a rebuilt module, another
|
||||||
|
session — the mesh would have composed every node without this one on the private network, and this
|
||||||
|
node without its own declaration.
|
||||||
|
|
||||||
|
## Why it matters beyond this instance
|
||||||
|
|
||||||
|
[ADR 0207](../../02-DECISIONS/0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md)
|
||||||
|
says `assign` judges the modules together and refuses what cannot be met. It judges seats. A
|
||||||
|
collision found only at composition — two modules declaring one package, one file, one port — is
|
||||||
|
reported after the assignment is written, as if it were a warning. The result is the most dangerous
|
||||||
|
state a node can be in: assigned, unresolvable, and silently dropped by the next push of anything.
|
||||||
|
|
||||||
|
The reason was also hard to read: one fault, said eleven times, with three follow-on complaints that
|
||||||
|
point at the network instead of at the collision.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
1. Should `assign` compose the node with the new assignments before writing them, and refuse the
|
||||||
|
whole call when composition fails? The node would then never become unresolvable through `assign`.
|
||||||
|
2. When a node does not resolve for any reason, should a push of other nodes keep its last composed
|
||||||
|
state in theirs rather than drop it from the private network?
|
||||||
|
3. Should one composition fault be reported once, with the follow-on unmet needs folded under it?
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-10-04
|
||||||
|
located-in: []
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 236 — The catalogue check passes a manifest the host refuses
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
2026-10-04. A login-manager module passed `mesh-controller module check`, was registered, built and
|
||||||
|
assigned. The first push of its declaration was refused whole by the host:
|
||||||
|
|
||||||
|
```
|
||||||
|
refused a declaration: this declaration is refused, and none of it was applied:
|
||||||
|
- resource "lemurs.service": a service that omits state leaves the unit's lifecycle to the
|
||||||
|
machine, and boot and takes-over are both its lifecycle
|
||||||
|
```
|
||||||
|
|
||||||
|
The rule is the host's declaration validation. The controller's check never applies it, so a manifest
|
||||||
|
can pass every check the catalogue has and still fail on the first machine that receives it.
|
||||||
|
|
||||||
|
## Why it matters beyond this instance
|
||||||
|
|
||||||
|
A refusal is whole, so one module with this fault blocks every other change for that node until the
|
||||||
|
module is fixed, rebuilt and pushed again. The fault is static: it is in the manifest, and could be
|
||||||
|
found before a module is merged. Today it is found by assigning the module to a live machine.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
1. Should the host's declaration validation be importable, so that `module check` (and registration)
|
||||||
|
runs it over each resource the manifest declares?
|
||||||
|
2. Or should the controller validate the composed declaration before it sends it, and refuse to send
|
||||||
|
one the host would refuse?
|
||||||
|
3. Which other host-side rules are not visible to the catalogue check today?
|
||||||
Reference in New Issue
Block a user