Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a8a378ef9 | ||
|
|
3f48e685cc | ||
|
|
1e0b9ee11f | ||
|
|
1faa63b2d5 | ||
|
|
3a359bf99e | ||
|
|
ec6d106af8 | ||
|
|
e2b4f3a5c4 | ||
|
|
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
|
||||
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
|
||||
|
||||
- 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
|
||||
`${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
|
||||
`~/.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)
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-10-04
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md
|
||||
---
|
||||
|
||||
# 211. A machine's power is a node seat, its moments take contributions, and its states are events
|
||||
|
||||
## Context
|
||||
|
||||
The laptop's module needs code to run around sleep:
|
||||
- the GPU driver's own suspend and resume actions;
|
||||
- a touchpad reset after waking.
|
||||
|
||||
It wrote drop-ins of its own into the service manager's sleep services, so it wrote into files that
|
||||
belong to another tool's holder. [ADR 0210](0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md)
|
||||
forbids exactly that. A second module wanting code after waking would do the same, and nothing would
|
||||
order the two or say that either needs sleep to be handled at all.
|
||||
|
||||
The mesh also cannot tell a sleeping machine from a lost one. A laptop with its lid closed stops its
|
||||
heartbeat exactly as a crashed machine does, and is reported "out of touch" either way.
|
||||
[Research 028](../01-RESEARCH/028-the-meshs-output-channel/00-overview.md) would turn every closed lid
|
||||
into an alert.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Each module writes its own sleep drop-ins,** as the laptop's did. Rejected by ADR 0210: no
|
||||
owner, no order, no dependency.
|
||||
2. **The service manager's holder takes power hooks.** Rejected: sleep and power are logind's and the
|
||||
firmware's concern, not service management's. On a laptop they also include lid, power source and
|
||||
battery, which the service manager knows nothing about.
|
||||
3. **A power seat on every machine.** Its holder:
|
||||
- owns the machine's power handling;
|
||||
- places code that modules contribute for named moments;
|
||||
- publishes the machine's power states as events.
|
||||
|
||||
Chosen. It was the operator's proposal.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. `node-power` is a node seat in the mesh's own set.** One module per node holds it. Every
|
||||
machine has one, servers included: every machine boots and shuts down. The first holder is a module
|
||||
named `power`.
|
||||
|
||||
**2. Its holder owns the machine's power handling:**
|
||||
- logind's power-key and lid settings;
|
||||
- the hooks around sleep, boot and shutdown;
|
||||
- the reading of power source and battery where the machine has them.
|
||||
|
||||
A model's specific values, such as what the lid does on that laptop, are the model's module's
|
||||
contribution or a setting of `power` per node (ADR 0174), never a second writer of logind's
|
||||
configuration.
|
||||
|
||||
**3. Modules contribute code for named moments.** The moments:
|
||||
- after boot;
|
||||
- before sleep;
|
||||
- after waking;
|
||||
- before shutdown;
|
||||
- on mains power;
|
||||
- on battery.
|
||||
|
||||
A contribution is POSIX shell code, written with ADR 0204's mechanism as ADR 0208 §4 did for the
|
||||
session's start:
|
||||
- a `shell` contribution whose `for` names the moment, in the `first`, `normal` or `last` slot;
|
||||
- placed by the holder with `${shell:<moment>:<slot>}` in the scripts its own units run;
|
||||
- run as root, in module order, each piece bounded in time, so that one module's hang cannot hold a
|
||||
machine awake.
|
||||
|
||||
Per ADR 0210, a contribution for a moment depends on `node-power`.
|
||||
|
||||
**4. Its states are the holder's events, on the bus:**
|
||||
- `booted`, `sleeping`, `woke`, `shutting-down`;
|
||||
- `on-mains`, `on-battery`, `battery-low`, where the machine has a battery.
|
||||
|
||||
They carry the machine's role and a time, and nothing secret, so any node and the controller may
|
||||
consume them.
|
||||
- **`sleeping` is published before the machine sleeps.** The holder takes logind's delay lock,
|
||||
publishes, and releases the lock once the bus has acknowledged, within logind's delay bound.
|
||||
- **On waking,** the holder queues events until the bus is reachable, then publishes them in order.
|
||||
|
||||
**5. A machine that said `sleeping` is asleep, not out of touch,** until it says `woke` or misses its
|
||||
expected return. The controller shows the state, and the output channel (research 028) does not
|
||||
treat a sleeping machine as a fault.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The laptop's module moves its sleep drop-ins into contributions:
|
||||
- the GPU driver's suspend and resume actions before sleep and after waking;
|
||||
- its touchpad reset after waking.
|
||||
|
||||
Its own files in the service manager's directories go.
|
||||
- Assigning `power` to every machine is phase 1 of [to-be 42](../03-DESIGN/01-to-be/42-the-machines-modules-in-order.md).
|
||||
- The controller gains the moments as contribution targets placed by `node-power`, and a node's
|
||||
power state in what it shows about the node.
|
||||
- **What got harder:**
|
||||
- code that must run at a precise point inside the sleep transaction cannot be a contribution; the
|
||||
GPU driver's own units are an example. Such code still declares its own units, and only the
|
||||
request to run them is contributed;
|
||||
- an event published around sleep depends on the network still being up. The delay lock buys the
|
||||
time, and if the bus does not answer within it, the machine sleeps anyway and says so on waking.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A moment's contribution derives a dependency on `node-power`, and lands in that moment's placeholder in module order | the controller's contribution tests |
|
||||
| A contribution naming an unknown moment is refused | the catalogue check |
|
||||
| One piece of hook code that hangs is ended after its bound, and the next still runs | the power module's tests over real child processes |
|
||||
| `sleeping` is published before sleep and `woke` after, and a missed acknowledgement does not hold the machine awake | the power module's tests with a fake logind and bus, and a live suspend of the laptop |
|
||||
| A machine that said `sleeping` is not reported out of touch | the controller's status test |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md),
|
||||
[ADR 0198](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md),
|
||||
[ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md),
|
||||
[ADR 0208](0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md),
|
||||
[ADR 0210](0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md)
|
||||
- [Research 028](../01-RESEARCH/028-the-meshs-output-channel/00-overview.md)
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-04
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md
|
||||
---
|
||||
|
||||
# 212. A seat says what it receives, and the machine's hotkeys are a seat
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0210](0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md)
|
||||
decided that a tool's configuration belongs to its seat's holder, and that every other module extends
|
||||
it with a contribution to the seat (§2), in a grain the seat defines (§5). The controller knows only
|
||||
three such grains:
|
||||
- the environment ([ADR 0203](0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md));
|
||||
- shell code in named slots ([ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md));
|
||||
- the power moments ([ADR 0211](0211-a-machines-power-is-a-node-seat-its-moments-take-contributions-and-its-states-are-events.md)).
|
||||
|
||||
Each was a field of its own, with a renderer of its own. Two more appeared on the first workstation:
|
||||
|
||||
- **The window manager.** Four modules, the launcher, the clipboard, the wallpaper and the bar, wrote
|
||||
files of their own into its include directory, and so did the laptop's model module. That is what
|
||||
ADR 0210 forbids.
|
||||
- **The keys the window manager never sees.** A laptop's vendor keys reach only a hotkey daemon, which
|
||||
reads trigger lines (a key, a state, a command). The daemon's configuration was the laptop module's,
|
||||
although the daemon is a general piece that more than one module has keys for.
|
||||
|
||||
A field and a renderer per grain would make every new seat a change to the controller's schema.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **A field per grain,** as before. Rejected: the manifest and the controller grow with every seat
|
||||
that takes contributions.
|
||||
2. **Contributions as files in the holder's drop-in directory,** each contributor writing its own.
|
||||
Rejected by ADR 0210: a path in another module's territory.
|
||||
3. **One general contribution: a seat, a kind the seat receives, and text in the tool's own grammar.**
|
||||
The seat lists the kinds it receives. The holder places each kind with one placeholder, and the
|
||||
controller concatenates the contributions in module order, each under a comment naming its module.
|
||||
Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. A module contributes with `contributions`.** Each entry names:
|
||||
- a **seat**;
|
||||
- a **kind**, which that seat receives;
|
||||
- **content**, text in the tool's own grammar, which the controller does not read.
|
||||
|
||||
**2. A seat lists the kinds it receives,** with the comment prefix of its tool's grammar. A
|
||||
contribution of a kind its seat does not list is refused at registration.
|
||||
|
||||
**3. The holder places a kind with `${contribution:<seat>:<kind>}`** in its own files. The placeholder
|
||||
is filled with every module's contribution of that kind on the node:
|
||||
- in module order;
|
||||
- each preceded by a comment line naming the module;
|
||||
- empty when there is none.
|
||||
|
||||
A placeholder in a module that does not claim the seat is refused, as ADR 0204 refuses shell slots.
|
||||
|
||||
**4. A contribution depends on its seat** (ADR 0210 §3), derived and refused as
|
||||
[ADR 0207](0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md) says.
|
||||
|
||||
**5. Two seats receive first:**
|
||||
|
||||
| seat | kind | what it is |
|
||||
|---|---|---|
|
||||
| `node-display-session` | `config` | window-manager configuration lines: bindings, start-up commands, rules |
|
||||
| `node-hotkeys` (new, node scope) | `trigger` | hotkey-daemon trigger lines: a key, a state, a command |
|
||||
|
||||
`node-hotkeys` is in the mesh's own set. Its holder runs the daemon that sees the keys the window
|
||||
manager does not, and owns that daemon's configuration and service.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The window-manager fragments become `config` contributions of their modules: the launcher, the
|
||||
clipboard, the wallpaper, the bar and the laptop model. The window manager's module places them
|
||||
instead of including other modules' files.
|
||||
- A hotkey module holds `node-hotkeys`. The laptop's model module contributes its vendor keys
|
||||
instead of writing the daemon's trigger file, and keeps only what is its own: the scripts the keys
|
||||
run.
|
||||
- The three earlier grains stay as they are. Folding them into this form is a later change, not
|
||||
required by this record.
|
||||
- **What got harder:** a contribution is text the controller does not read, so a malformed line
|
||||
reaches the tool. The holder checks the composed file with the tool's own check where the tool has
|
||||
one (the window manager's), before it reloads.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A contribution names a seat and a kind that seat receives | the catalogue check, which registration runs |
|
||||
| The placeholder fills with every module's contribution, in module order, each named | the controller's contribution tests |
|
||||
| A placeholder outside the seat's holder is refused | the catalogue check |
|
||||
| A contribution derives a dependency on its seat | the controller's resolve tests |
|
||||
| `node-hotkeys` is a node seat of the mesh's own set | the seat table's tests |
|
||||
|
||||
## 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 0208](0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md),
|
||||
[ADR 0210](0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md),
|
||||
[ADR 0211](0211-a-machines-power-is-a-node-seat-its-moments-take-contributions-and-its-states-are-events.md)
|
||||
- [To-be 42](../03-DESIGN/01-to-be/42-the-machines-modules-in-order.md)
|
||||
@@ -192,6 +192,8 @@ 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)
|
||||
- **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)
|
||||
- **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)
|
||||
- **0212** — [A seat says what it receives, and the machine's hotkeys are a seat](0212-a-seat-says-what-it-receives-and-the-machines-hotkeys-are-a-seat.md)
|
||||
|
||||
### Its tiers, from the bottom up
|
||||
|
||||
@@ -307,6 +309,8 @@ 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)
|
||||
- **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)
|
||||
- **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)
|
||||
- **0211** — [A machine's power is a node seat, its moments take contributions, and its states are events](0211-a-machines-power-is-a-node-seat-its-moments-take-contributions-and-its-states-are-events.md)
|
||||
|
||||
### How it is built
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
layer: as-is
|
||||
status: implemented
|
||||
code: [mesh-controller internal/catalogue/state.go, mesh-controller internal/broker/state.go, mesh-controller cmd/mesh-controller/push.go, mesh-tools node-tools/internal/bus/state.go, mesh-tools node-tools/internal/launch/launch.go, mesh-sdk src/state, mesh-sdk go/state.go]
|
||||
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
|
||||
---
|
||||
|
||||
# A module's state, as it runs
|
||||
|
||||
**A module keeps the current value of something on the bus, and every machine sees it — including one
|
||||
that joins later.** Since 2026-10-04 a manifest may say `state` (buckets the module owns) and `reads`
|
||||
(another module's, as `<module>.<name>`). The first two modules to use it are the operator's agent on a
|
||||
machine and its licence manager; on the day this was written, three buckets existed on the bus.
|
||||
|
||||
## What runs
|
||||
|
||||
- **The controller** creates a key-value bucket `<module>_<name>` for every declared state, from the
|
||||
catalogue — on every start and, since the first module that declared state found it missing, on every
|
||||
push before the memberships that name it. A bucket nothing declares any more is reported and kept.
|
||||
Every bucket carries the mesh's caps: 256 KiB a value, 64 MiB a bucket.
|
||||
- **The grants**: the machine's runtime is granted, for each bucket a module it carries owns, writing
|
||||
under the bucket's own subjects and reading; for a bucket it only reads, reading. Measured once built,
|
||||
with the composed grants loaded into a server: a reader's write is refused by the server.
|
||||
- **The membership** issued to each assignment lists its buckets by the names the module uses, and
|
||||
whether it may write.
|
||||
- **The runtime** answers `mesh/state.get`, `put`, `delete`, `keys` and `watch` on the bundle's channel.
|
||||
A watch hands the current values — none that is deleted — then every change, each naming the watch it
|
||||
belongs to; it is answered once the current values are delivered. The runtime refuses, with the
|
||||
reason, a state the module was not issued, a reader's write, a key the bus cannot hold, and a value
|
||||
carrying a field named like a credential.
|
||||
- **The SDKs**: `state(name)` in TypeScript, `stdio.State(name)` in Go (tag `go/v0.1.7` and later).
|
||||
|
||||
## What the first live use showed
|
||||
|
||||
- **A refused request is a timeout, not a refusal.** The bus reloads a machine's grants a moment after
|
||||
the push that changed them; a bundle that asks in between waits out its deadline. A module that
|
||||
watches at start therefore watches beside its handshake and asks again until the state answers — the
|
||||
agent module needed two to seven attempts on its first start on each machine.
|
||||
- **A late machine reads the whole set.** A server registered for every machine before one machine was
|
||||
assigned the module reached that machine from the current values at its start.
|
||||
- **The secrets guard is partial and works for what it covers**: an entry carrying an `Authorization`
|
||||
header was refused on the live bus. A sealed value is plain text to an inspector, and is not caught.
|
||||
|
||||
## How it is checked
|
||||
|
||||
The controller's catalogue and broker tests (names, grants, memberships, a bucket asserted in place
|
||||
against a real server); the runtime's tests over a real bus (current values without deletions, refusals,
|
||||
the TypeScript SDK through the runtime); `module check` names a read whose owner on the shelf keeps no
|
||||
such state.
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
layer: as-is
|
||||
status: implemented
|
||||
code: [mesh-catalog modules/claude-code, mesh-catalog modules/claude-licence-manager]
|
||||
updated: 2026-10-04
|
||||
decisions:
|
||||
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.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/0209-a-login-on-a-node-moves-that-node-to-its-account-and-an-api-key-is-added-from-any-node-sealed.md
|
||||
---
|
||||
|
||||
# The operator's agent and its licences, as they run
|
||||
|
||||
**Every machine with an operator account runs the agent module, and one licence manager on the control
|
||||
node keeps the licences.** Both are Go binaries the machine's runtime launches; neither has a container,
|
||||
a port or a bus credential of its own. Live since 2026-10-04, on all four machines.
|
||||
|
||||
## The agent module on each machine
|
||||
|
||||
- **Writes the agent's managed directory**: the tool servers — the console as `mesh`, plus servers
|
||||
registered through the module — the mesh's settings, and the instruction file. The tool-server list
|
||||
is exclusive by the vendor's rule: a server not in it does not load on that machine.
|
||||
- **Keeps registered tool servers in its state**, one key per registration for every machine or for one;
|
||||
each machine renders what applies to it.
|
||||
- **Reports what its machine holds** — the account its agent names, the kind, fingerprints and expiries,
|
||||
never a token — at start and whenever the credentials file changes.
|
||||
- **Writes what the machine should hold**: on a newer generation of its binding it asks the seat's
|
||||
`current`, sealed to its own key, and writes the access token only. No machine holds a refresh token.
|
||||
- **Hands over a login when asked**, sealed to the manager's key, and **adds an API key** from a file on
|
||||
its machine the same way, removing the file once the manager has it.
|
||||
|
||||
## The licence manager on the control node
|
||||
|
||||
- **Holds the `anthropic-licence-manager` seat**: `licences`, `bindings`, `bind`, `switch`, `release`,
|
||||
`refresh`, `usage`, `adopt`, `public-key`, `current`.
|
||||
- **Learns licences from the reports**: a refresh token it does not hold is adopted by refreshing it,
|
||||
newest login first, once per account. The machine a login was made on is moved to that login's
|
||||
account; a machine bound to nothing is bound to the account it reports.
|
||||
- **Is the only refresher**: every exchange under a lease per licence in its own database, every four
|
||||
hours and in any case within an hour of expiry; grants are encrypted at rest with a key the vault made.
|
||||
- **Publishes what each machine should hold** as its `bindings` state, with a generation that grows with
|
||||
every rotation and switch.
|
||||
|
||||
## On the day it went live
|
||||
|
||||
One subscription account was adopted from the control node's own login on its first start; the other
|
||||
three machines, logged in to the same account with older logins, were bound to it without their logins
|
||||
being exchanged. A forced rotation reached all four machines within seconds. Two faults were found and
|
||||
fixed during the rollout: a machine reporting an already-adopted account later was never bound, and a
|
||||
seat verb named with an underscore was refused by the builder.
|
||||
|
||||
## How it is checked
|
||||
|
||||
Each module's own tests (the agent's instruction file held byte for byte to the renderer it replaced; the
|
||||
manager's rules on a store in memory and a stub vendor; its store against a real database); one run of
|
||||
both binaries under the real runtime with a stub vendor before going live; and live: `licences` lists the
|
||||
licence with every machine bound, and each machine's `claude_code_status` names it with no login waiting.
|
||||
@@ -22,6 +22,8 @@ Where the two disagree, the implementation wins and the disagreement is stated.
|
||||
| [`11-the-lab.md`](11-the-lab.md) | The lab — the first piece of the new shape that exists, and what it does not yet do |
|
||||
| [`12-the-seats.md`](12-the-seats.md) | The seats the mesh defines, who holds one, and where a seat changes resolution |
|
||||
| [`13-the-console.md`](13-the-console.md) | The mesh's tools on the machine a person sits at, served by a module the mesh assigned there |
|
||||
| [`14-a-modules-state.md`](14-a-modules-state.md) | A module's current state on the bus: what the controller creates, the runtime serves, and the first live use showed |
|
||||
| [`15-the-agent-and-its-licences.md`](15-the-agent-and-its-licences.md) | The operator's agent on every machine and the licence manager that keeps its licences |
|
||||
|
||||
## What these documents are not
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
status: implemented
|
||||
code: [mesh-catalog modules/claude-code]
|
||||
updated: 2026-10-04
|
||||
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/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
|
||||
@@ -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
|
||||
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;
|
||||
- **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
|
||||
the file matches what was handed over — by fingerprint, never by value.
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
status: implemented
|
||||
code: [mesh-catalog modules/claude-licence-manager]
|
||||
updated: 2026-10-04
|
||||
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/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
|
||||
@@ -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
|
||||
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`.
|
||||
- **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
|
||||
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
|
||||
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.
|
||||
- **An API key** enters from any node (*ADR 0209*): the agent module there reads it from a file on its own
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
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.
|
||||
|
||||
## 8. Settings
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
status: implemented
|
||||
code: [mesh-catalog modules/claude-code, mesh-catalog modules/claude-licence-manager]
|
||||
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
|
||||
|
||||
@@ -15,6 +15,9 @@ decisions:
|
||||
- 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
|
||||
- 02-DECISIONS/0212-a-seat-says-what-it-receives-and-the-machines-hotkeys-are-a-seat.md
|
||||
- 02-DECISIONS/0211-a-machines-power-is-a-node-seat-its-moments-take-contributions-and-its-states-are-events.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
|
||||
@@ -76,6 +79,15 @@ 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.
|
||||
|
||||
**Added 2026-10-04.** Two more modules for every machine:
|
||||
- `power` holds `node-power` ([ADR 0211](../../02-DECISIONS/0211-a-machines-power-is-a-node-seat-its-moments-take-contributions-and-its-states-are-events.md)). Other modules contribute code for
|
||||
its moments (after boot, before sleep, after waking, before shutdown, on mains, on battery), and it
|
||||
publishes the machine's power states on the bus.
|
||||
- `dbus` holds the message bus. Modules shipping D-Bus policies or services contribute them to it.
|
||||
It shares curated events, never raw traffic. An upgrade never restarts the bus live: its package
|
||||
waits for a reboot. A live restart in the middle of a full upgrade took down a workstation's
|
||||
logins on the day this was written.
|
||||
|
||||
## Phase 2 — both workstations
|
||||
|
||||
In order:
|
||||
@@ -93,6 +105,18 @@ 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).
|
||||
|
||||
**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.
|
||||
|
||||
**Added 2026-10-04.** `triggerhappy` holds `node-hotkeys` on both workstations ([ADR 0212](../../02-DECISIONS/0212-a-seat-says-what-it-receives-and-the-machines-hotkeys-are-a-seat.md)). The
|
||||
laptop model's vendor keys become its contribution. The window-manager fragments of the launcher, the
|
||||
clipboard, the wallpaper, the bar and the laptop model become `config` contributions to
|
||||
`node-display-session`.
|
||||
|
||||
## Phase 3 — one machine model
|
||||
|
||||
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