Merge remote-tracking branch 'origin/main' into decision/0272-mesh-cli-the-operators-command
This commit is contained in:
+56
@@ -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.
|
||||
Reference in New Issue
Block a user