Merge pull request 'ADR 0260: a bar block shows a value its module keeps on the bus, and no module reads another's files' (#212) from feat/the-bar-takes-blocks into main

This commit was merged in pull request #212.
This commit is contained in:
2026-10-08 14:09:26 +00:00
3 changed files with 151 additions and 0 deletions
@@ -150,6 +150,10 @@ PLATYPUS attack, CVE-2020-8694), which is why the kernel makes it root's. No per
has no way to condition a resource on another seat (the bar) being held on the machine, and building
one for a process that reads a few kernel files every five seconds is not worth a new mechanism now.
> **Extended — 2026-10-08, by [ADR 0260](0260-a-bar-block-shows-a-value-its-module-keeps-on-the-bus-and-no-module-reads-anothers-files.md).** Modules talk over the bus. The bar no longer
> prints the power module's file: the power module keeps the draw as its state on the bus, and the bar
> is granted the read. A block may `show` such a state (§5).
**10. The memory block shows what programs hold,** not counting the cache. That is the figure
`memory-pressure` judges the same machine by.
@@ -0,0 +1,139 @@
---
topic: what runs on it
status: accepted
date: 2026-10-08
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md
---
# 260. A bar block shows a value its module keeps on the bus, and no module reads another's files
## Context
[ADR 0255](0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md)
gave the bar its power draw this way (§9):
- the power module's root sampler wrote one line to a file under its own runtime directory;
- the bar's block ran a command of the power module that printed that file.
That is two modules talking through a file. On 2026-10-08 the operator restated a core principle:
all communication between modules goes over the bus (NATS). The bus is where grants say who may say
what, where an exchange can be seen, and what keeps working when a module is replaced. A file between
two modules has none of that. It also binds each module to the other's paths.
What the mesh already had:
- **A module keeps state on the bus** ([ADR 0201](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)).
It declares the state under `state`. Another module declares what it reads under `reads`, as
`<module>.<name>`. The runtime refuses anything not issued, and a reader may never write.
- **A module's bundle reaches the bus through the node's runtime** (ADR 0198). The runtime tells it
which machine it is on.
- **The energy counter is root's** (ADR 0255 §9). The bundle runs as the operator's account and cannot
read it. The root process can read it and cannot reach the bus.
## Considered Options
1. **The power module publishes the draw as state, and the bar's bundle watches it.** The bar's block
shows a value the contributor keeps on the bus. Chosen.
2. **An event every five seconds.** Rejected on bus load and meaning. A reading is a current value that
is replaced every five seconds, and nobody needs the earlier ones. An event is history: each one is
kept in a stream until its age runs out, and a bar that starts later still has no value until the
next one arrives. State keeps one value per machine, and a reader sees it at once.
3. **Keep the file, and make it the bar's.** Rejected: the power module would write into the bar's
directory. That is a path in another module's territory, which ADR 0210 already refuses.
## Decision
**1. Modules talk over the bus, never through each other's files.** No module names a path that
another module's manifest places under `/run`, `/var/lib`, `/usr/local/lib` or `/etc`. Within one
module, parts may pass a file: its root process and its bundle are one module.
Two exceptions are written down beside the check:
- a short list of paths every program on a machine reads as the machine's own, such as `/etc/hosts`;
- the cross-module reads that stood on the day the rule took `/etc` in. These may only go: there are
five, all the laptop model's module reading the power module's and the hotkey daemon's files.
A path a container sees inside itself is the container's own. The check reads text, so it cannot see
a path built in code from pieces, read from a setting, or reached through a shell the module runs.
**2. A block may show a value its module keeps on the bus.** This adds `shows: state` to ADR 0255 §5:
- options `state`, naming the state as `<module>.<name>`;
- option `key`, lower-case letters, digits and hyphens, the machine's own name when absent;
- the value is an object, and its `text` is what is shown.
A module offers only state it keeps itself. A block naming another module's state, or one the
contributor does not keep, is refused at registration. The holder's template sees `machine`, the node
it renders for, so the key needs no setting.
**3. The holder is granted the read of one key, without naming the contributor.** The controller
derives the holder's reads per machine: each state that the modules on the holder's machine offer in
pieces of a kind its seat receives as data, and of each only the key the piece shows. The bar's manifest
names no power module, and a bar on a machine without the power module reads nothing of it.
- On the bus the grant is that one key: a direct get of its subject, and a consumer filtered to it.
A client watching one key creates its consumer by the create-with-filter form, which the server
holds to the filter it names.
- Binding to the bucket, deleting a consumer and flow control stay per bucket. None of them reads or
writes a value.
- **State may be per machine.** A state declared `per-machine` is written and read by each machine's
instance at the key named for its machine, and the bus grants that key and no other. The power
module's `draw` is per machine, so a machine's power module can write only its own draw.
- One account per machine carries every module there, so the bus enforces the union of their grants.
For the power draw, that union is still the machine's one key.
- The membership names the key, so the runtime can refuse first. Until it does, the bus refuses.
**4. The power draw is the power module's state `draw`, per machine.** It holds one value
per machine, with these fields:
- `text`, the line shown;
- `watts`;
- `measures`: `machine`, `cpu`, `cpu+gpu` or `gpu`;
- `at`, when it was measured.
A value lives 30 seconds. The root sampler leaves each reading in the module's own runtime directory.
The module's bundle puts a fresh reading that measured something; a stale or empty one is not put. On
four machines that is under one message a second.
**5. The bar shows a bus value in its own way.** For i3status-rust:
- the bar's bundle watches each state its bars name;
- it leaves the value's text in the bar module's own runtime directory;
- it dates each value by its own `at`, not by when it arrived;
- the block runs the bar module's own command, which prints the text, or nothing when the value was
measured more than 30 seconds ago, so a value frozen at its source hides;
- a watch that says nothing for twice the value's lifetime is read again, and the bundle says so in its
log;
- a refused watch is logged and tried again;
- a value no bar shows any more is dropped. The SDK has no way to end a watch, so a dropped watch
lasts until the bundle restarts, and what it hands over is ignored.
Another bar module would show the same state in its own way.
**6. The battery block stays the bar's own.** i3status-rust's battery block reads the kernel's
power-supply class itself. That is the bar reading the machine, not another module, and it is allowed.
## Consequences
- ADR 0255 §9's file between the power module and the bar is gone. Its sampler, its root-only counter
and its honest labels stand.
- The `command` kind of block stays, for a bar's own commands. A module offering a value to the bar
offers state.
- The derived read needs the controller that derives it. Until it runs, the bar's watch is refused by
the runtime. The bar's bundle tries again every half minute, and the block stays hidden.
- **What got harder:** a value from the bus passes through two bundles and the runtime, so it takes
longer to arrive than a file read. It arrives within one sampling interval.
## How it is checked
| Rule | Checked by |
|---|---|
| No module names a path under `/run`, `/var/lib`, `/usr/local/lib` or `/etc` that another module's manifest places; the known exceptions only shrink | the catalogue's `checks/paths` with its `shared` and `known` lists, run by its `merge-check.sh` over every module, and that check's own tests |
| A block shows only state its contributor keeps | the catalogue check; the controller's `TestAModuleOffersItsOwnStateOnly` |
| The holder is granted the one key each piece on its machine shows, and nothing more | `TestAStateBlockIsRenderedForThisMachineAndTheHolderIsGrantedToReadIt`; `TestABarIsGrantedTheStateItsMachinesModulesOfferIt` against a real store |
| A keyed read and per-machine state reach one key on the bus, also as the machine's union | the controller's `TestPerMachineStateAndAKeyedReadReachOneKey`, `TestAMembershipNamesTheOneKeyOfKeyedState` |
| Only a fresh reading that measured something is put, keyed by the machine | the power module's `publish_test.go` |
| The bar dates a value by its reading, says a refused or silent watch, rereads a silent one, drops a removed block, and hides a value measured more than 30 s ago | the i3status-rust module's `values_test.go` |
## References
- [ADR 0255](0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md) §5 and §9, which this extends
- [ADR 0201](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md), state on the bus
- [ADR 0210](0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md)
- [To-be 42](../03-DESIGN/01-to-be/42-the-machines-modules-in-order.md)
@@ -20,6 +20,7 @@ decisions:
- 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
- 02-DECISIONS/0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md
- 02-DECISIONS/0260-a-bar-block-shows-a-value-its-module-keeps-on-the-bus-and-no-module-reads-anothers-files.md
---
# 42. The machines' modules, in order
@@ -131,6 +132,13 @@ both places of both bars once. The bar writes no block that depends on the hardw
be its block.
- The bar's memory block shows what programs hold, the figure `memory-pressure` judges by.
**Changed 2026-10-08** ([ADR 0260](../../02-DECISIONS/0260-a-bar-block-shows-a-value-its-module-keeps-on-the-bus-and-no-module-reads-anothers-files.md)): modules talk over the bus. The power draw is
the power module's state `power.draw`, one value per machine, which its bundle puts from its own root
sampler. The block `shows: state`; the bar's bundle watches it under a read the controller grants the
bar for what the modules on its machine offer, and the bar names no module. The battery block is the
bar's own, reading the kernel. The catalogue's `checks/paths` refuses a module that names another
module's path under `/run` or `/var/lib`.
The node-engine is delivered first, so the machines report the two capabilities. The controller comes
next, because it parses `data`, `if-capability` and `renders`. The catalogue comes last. Left open: the
draw at the wall (a metered plug or a UPS), a GPU-load block from a generic module, and the headsets.