Merge remote-tracking branch 'origin/main' into decision/0272-mesh-cli-the-operators-command

This commit is contained in:
jochen
2026-10-09 13:15:36 +02:00
8 changed files with 853 additions and 2 deletions
@@ -0,0 +1,56 @@
---
status: located
opened: 2026-10-08
located-in: [mesh-catalog modules/claude-code, mesh-catalog modules/sudo, mesh-controller internal/catalogue, mesh-host internal/accounts]
fixed-by:
amended-design: 03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md
---
# 333. An agent on the control node can become root without a person
## Symptom
On 2026-10-08 the `sudo` module's `sudo_check` on the control node answered, for the account every coding agent
there runs as:
- `passwordless: true`, decided by the module's drop-in, which grants the operator account every command as
root without a password;
- a second grant of the same, set by hand in the machine's main sudoers file before the module existed;
- the groups `docker` and the account's own. Membership of `docker` is root without sudo: a container started
with the machine's root filesystem mounted.
So every agent session on the control node can become root without a person. It runs as the same account as
the tool runner, so it can also read the runner's bus credential, which carries the grants of every module the
runner serves.
## Why it matters
ADR 0259 (hq pull request 211, not yet merged)
§8, revised after the security review of 2026-10-08 (hq pull request 211), lets the router issue an approve
warrant, and honours a channel's `verified-sender`, only on a machine where the self-check finds that no account
an agent runs as can become root without a person. The router and the first channel are placed on the control
node. There the condition fails, so an agent could publish a warrant, or answer for the operator, with the
runner's credential or as root.
## What the design let pass
- **Nothing measured it.** The `sudo` module reports what the operator account may do. No check asks which
account an agent runs as, or whether that account can become root.
- **One account did three jobs**: the person's login, the tool runner, and every agent. ADR 0181 decided one
account per node and left several open.
## Where it is located
| part | owner |
|---|---|
| which account the agent runs as, and where its files go | mesh-catalog `modules/claude-code` |
| the operator account's rule | mesh-catalog `modules/sudo` |
| a node naming a second account, and the self-check's invariant | mesh-controller `internal/inventory`, `internal/catalogue`, `cmd/mesh-controller` |
| judging whether an account can become root | the node-engine, `mesh-host` `internal/accounts` |
## The decision
[ADR 0266](../../02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md):
on the control node, agents run under an account of their own. The node-engine judges on every look that the
account cannot become root without a person, and the self-check says it urgently when it can. The operator's
account keeps its sudo.
@@ -0,0 +1,64 @@
---
status: diagnosing
opened: 2026-10-09
located-in: []
fixed-by:
amended-design:
---
# 345. A calendar reminder shows as a transparent, empty window, and the mesh had no tool to look at it
## Symptom
On 2026-10-09 the operator reported that on the workstation, when an Outlook (Exchange) meeting is
due, the mail client's reminder window appears, but it is transparent and shows no content: no title,
no time, no Dismiss or Snooze. The mail client is Betterbird (a Thunderbird fork, installed from the
AUR), with the Exchange calendar through the Owl add-on. The workstation runs i3 and the `picom`
compositor (glx backend, vsync, fading). No module owned Betterbird.
The operator's view of done: *on the workstation, when an Outlook meeting is due, the reminder window
shows the meeting (title, time, Dismiss/Snooze) on a normal background, not a transparent empty window,
and the mesh has tools to see and fix Betterbird.*
## Reported history
The operator reports (2026-10-09) earlier trouble with picom on the workstations, never recorded: a chat
window (Telegram) froze, not redrawn, after a change of i3 workspace. `records_search` for picom finds
nothing about it. It is the same kind of failure as this one, a window the compositor shows without its
content, and it makes picom the first suspect here.
## Why it is an issue
Two gaps, and the symptom itself.
1. **A desktop application the operator uses every day ran outside the mesh.** Nothing could say which
window the reminder is, what the compositor makes of it, or which preferences the mail client runs
with. A change to it could only be made by hand in its profile, which the mesh's rules forbid and
which nothing would keep.
2. **The tools that look at a screen could not look at this window.** The window manager's list
(`node-display-session.windows`) leaves out every window it does not tile, the reminder among them,
and says nothing a compositor decides by (a window's depth, type, opacity). The screenshot tool of the
display server failed for one window with `X Error BadColor (invalid Colormap parameter)
X_QueryColors`, because it dumped the window by its id.
3. **The symptom's cause is not known yet.** A reminder that shows nothing is a missed meeting.
The diagnosis is in [01-diagnosis.md](01-diagnosis.md).
## Impact
- The operator cannot read or dismiss a meeting reminder: the window is there but empty.
- Until now, the next session that looked at it had to work around the mesh to see a window.
## What is done
- The display server's module gains a window list that includes unmanaged windows with their depth,
type, state, role and own opacity, and its screenshot of one window is cut from the screen as the
compositor shows it (catalogue pull request, linked from this record's pull request).
- A `betterbird` module: status, named preferences without credentials or accounts, a user.js declared
through the module's setting, its log, restart, check, and a reminder on demand from a throw-away
profile, so a fix can be proved without touching the operator's calendar.
- The `picom` module's backend and damage tracking become settings of each machine (defaults unchanged:
`glx`, `use-damage` true), so the candidate fixes can be set and proved without a hand edit.
What is not done: the cause, and so the fix. See the diagnosis.
@@ -0,0 +1,106 @@
# 345 — Diagnosis
All times 2026-10-09, local time of the workstation. "Measured" is what a tool answered; "guess" is
labelled as one.
## The record first
`records_search` for the symptom's words, `betterbird`, `thunderbird`, `picom transparent` and
`BadColor X_QueryColors xwd`: no hits. Nothing in the record covered this.
## What was measured
**The client.** `betterbird-bin` 153.1.0esr-1 (application 153.1.0, build 20260820183427), foreign to the
official repositories (from the AUR), on the workstation and on the laptop. On the workstation the main
process had run since 2026-10-04 16:05, in the login session's scope: started by hand from the launcher,
with no autostart entry and no line in i3's configuration. Its standard output is the login session's
pipe that nobody reads (the same pipe as the compositor's); its standard error is `/dev/null`. So nothing
it printed in five days was kept.
**The profile in use.** Betterbird holds the installation's default profile open (99 open files), named
by `[Install…] Default` in `profiles.ini`. Another profile is marked `Default=1`; it is not the one in
use. The profile has no `user.js`.
**Preferences that bear on how a window looks** (prefs.js, names and values only):
`browser.display.use_system_colors` true; `extensions.activeThemeID` the built-in compact dark theme;
`browser.theme.content-theme` and `toolbar-theme` 0; `mail.dark-reader.enabled` false. No `gfx.*`,
`layers.*`, `widget.*` or `alerts.*` preference is set, so the renderer and the notification path are
Betterbird's defaults. The one add-on the operator installed is Owl (Exchange). The reminder dialog's
saved place (`xulstore.json`) is 600×300 on the right-hand monitor.
**The renderer.** Both the long-running process and a fresh one load the GL libraries and run
WebRender's threads (`Renderer`, `WRRenderBackend`, `Compositor`): hardware WebRender, in-process.
The workstation did not suspend since boot, and the journal holds no GPU reset.
**The reminder window.** A reminder was raised without the operator's calendar: a second Betterbird with
`--no-remote` in a throw-away profile under the account's runtime directory, holding one local calendar
file with one event whose reminder was due 30 to 40 seconds after the start. Four runs:
| run | profile | window | depth | drawn |
|---|---|---|---|---|
| 1, 11:43 | Betterbird's defaults | class `Calendar` / `eu.betterbird.Betterbird`, role `AlarmWindow`, type `DIALOG`, 600×300, floating | 32 | not captured: it was snoozed from the desk at 11:44, before the image was taken (the calendar file recorded the snooze) |
| 2, 11:49 | the same; the snoozed reminder again | the same | 32 | **yes**: title, time, Snooze for, Dismiss, on a dark background |
| 3, 11:52 | with the operator's theme, `use_system_colors`, fonts and the dialog's saved place | the same | 32 | **yes** |
| 4, 11:54 | as 3, with `mozilla.widget.use-argb-visuals` false | the same | **still 32** | yes |
Each image was taken from the whole screen at the window's place and looked at. Every Betterbird window
on the workstation has depth 32 (an ARGB visual), the long-running one's mail window included.
**The graphics stack** (2026-10-09, read on the workstation; no mesh tool answers it yet, so it was read
with the login shell's command verb): an AMD Radeon RX 6950 XT (Navi 21) on the kernel's `amdgpu` driver,
the X server's `amdgpu` driver (`xf86-video-amdgpu` 25.0.0) on X.Org 1.21.1.24, Mesa 26.2.4 (`radeonsi`),
kernel 7.2.8, picom 13. picom runs with the `glx` backend, vsync, fading, and damage tracking at picom's
default (on).
**The screenshot tool's failure.** `xorg_screenshot` of the whole screen worked; of one window it
dumped the window by its id (`xwd -id`): that is the call reported failing with BadColor, and here it
answered BadWindow for a window that had just closed.
## What follows from it (guesses, each said as one)
- **Why "transparent" rather than black.** A window of depth 32 that its program never drew holds
zero alpha, and picom blends it with what is behind it: it looks transparent, with i3's frame around
it. That matches the report exactly. So the window is very likely **mapped but not painted** by
Betterbird. That is a guess from how compositing works; it was not seen, because the symptom did not
happen in four runs.
- **Not the preferences found.** The operator's theme and colour switch drew the reminder correctly in
a fresh process (run 3). The preference that would take away the alpha visual does not change the
depth in this version (run 4), so it is no lever.
- **Candidates not yet ruled out, the first suspect first:**
1. **picom.** The operator has seen picom leave a window stale after a workspace switch before (the
reported history). picom's `glx` backend with vsync and damage tracking on (its default) is a known
source of windows left unpainted or stale on some drivers: picom repaints only the regions it was
told changed, and a missed damage event leaves a window's area as it was, for a new window empty.
That fits a reminder that appears while the operator is on another workspace. Not yet seen
happening;
2. the long-running Betterbird: five days of uptime, with WebRender's per-window state recreated many
times (its thread names count to 15). A fresh process drew every reminder;
3. Owl's reminders, which the test cannot raise without the operator's account;
4. the moment it appears: a reminder raised while a full-screen window holds the focused workspace
(Slack and a game were full screen on the day), or on the right-hand monitor where it was saved.
## What would decide it
At the next real reminder that shows empty, before touching it:
1. `xorg_windows` with class `Calendar` on the workstation: the window's map state and depth.
2. `xorg_screenshot` with that window and `inline`: what the screen shows.
3. **`picom.picom_toggle` with `on` false: compositing off.** If the reminder's content appears, picom is
the cause. This needs no restart of Betterbird and loses nothing. `xorg_screenshot` again, then
`picom_toggle` with `on` true to put compositing back.
4. Only if the content does not appear with compositing off: `betterbird_status` (how long it has run),
then `betterbird_restart`. It refuses while a composer window is open, because the unsent message
would be lost; `discard_unsent` is given only on the operator's yes. Then `betterbird_reminder_test`
with `carry` of the theme shows whether a fresh process draws it.
**If picom is the cause**, the candidate fixes are the `picom` module's settings for that machine, never a
hand edit: `use-damage` false first (repaint the whole screen each frame), else `backend` `xrender`. Each
is proved on the workstation before the operator relies on it: set it, `picom_restart`, raise a reminder
with `betterbird_reminder_test` while on another workspace, switch back, and look with `xorg_screenshot`.
The reported chat-window freeze is checked the same way.
**If a fresh Betterbird draws it and picom is not the cause**, the fix is in Betterbird or its renderer, not
the mesh; a preference to try then (`gfx.webrender.software` true, declared through the `betterbird`
module's setting) is proved with the same test.
Until then this record stays `diagnosing`, and the acceptance sentence is not met.
@@ -0,0 +1,58 @@
---
status: located
opened: 2026-10-09
located-in: [mesh-controller (internal/catalogue/setting_into.go settingInto, internal/catalogue/bound_into_files.go plainly, internal/catalogue/terminal_keys.go), mesh-catalog (every file a module marks "trusted": false)]
fixed-by:
amended-design:
---
# 346. A setting a verb may set is pasted raw into its file, so an untrusted file is an injection point
**Severity: high.** Through a file whose format has a statement separator or quoting, any caller of the
settings verb can add a line of its own to a program's configuration.
## Symptom
A review on 2026-10-09 found this in a pull request (mesh-catalog #157, the `picom` module's `backend` and
`use-damage` settings, issue 345), before it merged. Nothing was run against a node.
The pull request marked `picom.conf` `"trusted": false`, so that a machine's backend could be set through
the `settings` verb ([issue 339](../339-a-verb-can-place-a-directory-at-the-machines-own/00-report.md)).
The file says `backend = "${setting:backend}";`. A value of
xrender"; write-pid-path = "<any path>
renders as `backend = "xrender"; write-pid-path = "<any path>";`, and picom then overwrites any file the
operator's account can write. The same tail adds `log-file`, a shader path, a window rule that hides
windows, or `backend = "dummy"`, which blanks the screen. The unquoted `use-damage = ${setting:use-damage};`
takes the same tail without the quote.
The pull request was changed to keep the file trusted (terminal-only). The defect is general.
## Why it is an issue
- The controller fills `${setting:…}` with the value **as it is** (`settingInto` → `plainly`): a string is
written verbatim. It refuses only a line break, a carriage return, another line end and a NUL
(`settingsHoldOneLine`, issue 339).
- A setting has a `kind` (`preference`) and a `default`, and **no type and no allowed values**. Nothing
says `backend` is one of `glx`, `xrender`, or that `use-damage` is a boolean.
- `"trusted": false` (issue 339) opens a file's settings to the `settings` verb. It was meant for "a file
nothing but its own person reads". But a file read only by the person's own program can still make that
program do anything its format allows. Whether a raw value is safe depends on the file's format, and
nothing judges the format.
So the rule "trusted: false where nothing trusts it" is not enough. A one-line value is safe only where
the format has no way to end a statement or a quoted string within a line.
## Impact
- Any caller of the `settings` verb (an agent session, a tool) can rewrite a desktop program's
configuration beyond the key it was given, as the operator's account.
- Two files were marked untrusted on main. The survey in [01-diagnosis.md](01-diagnosis.md) judges
both: the `xdg` module's `user-dirs.dirs` is open to the same injection, `dunst`'s `dunstrc` is not.
`user-dirs.dirs` is made trusted again for now (mesh-catalog #158).
## What is asked
The fix is proposed in the diagnosis and needs a decision record (settings get a type), then a controller
change. Until it ships, **no new file is marked `"trusted": false`.**
@@ -0,0 +1,58 @@
# 346 — Diagnosis
2026-10-09. Read from mesh-controller `4d60628` and mesh-catalog main `7339ce8`. Nothing was run against
a node.
## Where a value goes
1. The `settings` verb stores a layer of values per module and node. Before storing, and again when it
composes, `settingsHoldOneLine` refuses a line break, a carriage return, another line end or a NUL in any
string. Nothing else is judged.
2. `settingInto` (internal/catalogue/setting_into.go) replaces each `${setting:<key>}` in a file's content
with `plainly(value)` (internal/catalogue/bound_into_files.go). For a string that is the string itself; a
number is printed without trailing zeros; a boolean as `true`/`false`. No quoting and no escaping, for
any format.
3. `TerminalKeys` (internal/catalogue/terminal_keys.go) makes every key a file asks for terminal-only, unless
the file says `"trusted": false`. Then the verb may set it.
4. A module's `settings` entry has `kind`, `default` and `why`. There is no type, no range and no list of
allowed values, so neither the verb nor the composition can refuse `xrender"; …` for a backend.
## Every file marked "trusted": false on main
| module | file | the line | format | verdict |
|---|---|---|---|---|
| `dunst` | `~/.config/dunst/dunstrc` | `width = ${setting:width}` and `font = Inter ${setting:font-size}` | INI: one key per line, the value to the end of the line, no separator or quoting within a line | **not open** to adding a key: without a line break a value stays the value of its own key. A wrong value (`width = abc`) makes dunst fall back or refuse the file: an outage of notifications, not an injection |
| `xdg` | `~/.config/user-dirs.dirs` | `XDG_DESKTOP_DIR="${setting:desktop-dir}"`, and the same for `publicshare-dir` and `videos-dir` | shell assignments; the xdg-user-dirs format is defined to be sourced by a shell | **open.** A value of `$HOME/x"; <command>; X="` ends the quoted string and adds a command, and `$(<command>)` inside the quotes runs too, in every shell or script that sources the file. The programs measured on the laptop parse it instead (`xdg-user-dir` is a C program there, and GLib parses), and no managed file sources it; a script of the operator's, or a tool that follows the format's definition, would run the command. Even parsed, a value can point the desktop or public folder anywhere the account can write |
Mergeable JSON files ("any mergeable file takes any key", issue 340) are not pasted: values are merged as
JSON and encoded by the encoder. They are outside this defect.
**Done for now (2026-10-09):** `user-dirs.dirs` is marked trusted again (terminal-only) in mesh-catalog
#158, as `picom.conf` was kept in mesh-catalog #157. A test in each module fails if its file carries a
trusted marking, naming this issue. `dunstrc` stays as it is. This is the stop-gap, not the fix: the
fix below still stands.
## The fix proposed
A decision record, then a controller change. The coordinator schedules it; it is not built here.
1. **A setting declares a type** in the manifest: `bool`, `int` (with an optional range), `enum` (its
allowed values), or `string` with the **format it is written into**, for example `ini-value`,
`shell-double-quoted`, `libconfig-string`, `path`. A setting without a type is a string that only the
terminal may set.
2. **The controller validates at the verb**, before a layer is stored: a bool is a JSON boolean, an int an
integer in range, an enum one of its values, a string one line. A refusal names the key and the type.
Validated again at composition, for layers stored before.
3. **The controller quotes at render** with the format's own function: `shell-double-quoted` escapes
`"`, `\`, `$` and backquote; `libconfig-string` escapes `"` and `\`; `ini-value` refuses `;`, `#` and a
leading `[`, or quotes where the format allows. A bool, an int or an enum needs no quoting, because it
cannot hold one.
4. **`"trusted": false` requires every setting the file asks for to be typed**, checked by `module check`
in the catalogue's merge check, so an untyped string can never reach an untrusted file again.
5. `picom`'s `backend` becomes `enum [glx, xrender, egl]` and `use-damage` a `bool`; then `picom.conf` may
be untrusted. `xdg`'s three folders become `shell-double-quoted` paths under `$HOME/`.
**Until then, no new file is marked `"trusted": false`.** How that rule is checked: `module check` already
counts the files that ask for a setting without saying whether they are trusted; the catalogue's merge
check can refuse any newly added `"trusted": false` (a list that only shrinks, like `health-undeclared`).
That check is part of the fix, not built here.