Commit Graph
100 Commits
Author SHA1 Message Date
jschoubben f5158ce5bf telegram, desk-channel: move to mesh-sdk go/v0.1.10, the tag of mesh-sdk #12's merge
mesh/delivery delivered
mesh/merge-gate pass: builds dunst, messenger, sudo, new: modules/desk-channel, modules/telegram → ace, g14, novox, shanks; no bus step; every machine comp…
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
2026-10-09 17:08:17 +02:00
jschoubben f5cbfd850c rofi: write the hidden prompt from names and say the controller asks, never with the caller's words
mesh/merge-gate pass: builds rofi → g14, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
The review of 2026-10-09 (M4): the prompt and its message were the caller's free text, so whoever could
ask the verb could put any words before the operator. The verb now takes the module, the secret and the
machine, checked as names, and writes the prompt here, saying the mesh's controller asks — which the bus
makes true, denying the verb to every other principal (mesh-controller #156). prompt and message are refused.
2026-10-09 17:08:17 +02:00
jschoubben 41b9e2d613 telegram, desk-channel: move to mesh-sdk 16984aa, rebased on its main 2026-10-09 17:08:17 +02:00
jschoubben 723e5f8cce telegram: say the desktop client's gap is a known one the router does not enforce (hq issue 344) 2026-10-09 17:08:17 +02:00
jschoubben 0284204989 desk-channel: run as its own account, and show a link's code only where a notifier heard unpaused will show it, kept in no history
The review of 2026-10-09 (H4). The desk ran in the operator's runtime, where agents run; it now runs as
the account desk-channel, as telegram does. A link's code is shown only on a named machine whose notifier
said in the last five minutes that it is not paused, and is sent secret, so the notifier keeps it out of
the history every caller of the verbs reads; while no such machine exists the desk says it cannot send,
and the router starts no link.
2026-10-09 17:08:17 +02:00
jschoubben c161521e49 dunst: keep what is shown once out of the history, and say whether notifications are paused
The review of 2026-10-09 (H4): a link's code shown on the desk stayed in dunst's history, which
node-notifier.history serves to every caller of the mesh's verbs, agents among them; and a code sent to a
paused notifier is held and shown later, to whoever is then at the screen. send now takes secret (transient,
category mesh.secret) and transient; the dunstrc's rules keep both out of the history, and history leaves out
any that got there. The pause, read from dunst itself, is said as the event paused at start, on change and
every two minutes, for the desk to read.
2026-10-09 17:08:17 +02:00
jschoubben 711e674ab1 telegram: take a tap only on a message showing the words this channel put there, and say where an approval from Telegram means something
The review of 2026-10-09 (M4): another holder of the token can change a message's words and leave its
buttons, so the operator taps Release under words that say something else. Each message's words are now
kept as a digest, per chat and message, and a tap on a message whose words differ, or that this channel
never showed, is refused on the phone and never said to the router.

The README says, as the operator directed (H5), that an approval from Telegram is meaningful only where
agents run under their own account without sudo, without the operator's X display, and with Telegram
Desktop's data readable by the operator alone; until then, approve from the phone.
2026-10-09 17:08:17 +02:00
jschoubben d991ef6b55 telegram, desk-channel: move to mesh-sdk 7690299, whose digests are over named fields (the review of 2026-10-09) 2026-10-09 17:08:17 +02:00
jschoubben 76db269c83 telegram, desk-channel: move to mesh-sdk d4077b4, where a warrant names the ask's digest (hq ADR 0259 §6) 2026-10-09 17:08:17 +02:00
jschoubben 8754de747d messenger: judge the break-glass file's owner by a faked owner both ways, so the test holds under root
mesh/merge-gate pass: builds messenger, sudo → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compo…
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery stopped: the pull request closed unmerged
The build seat runs the suite as root, where every file the test makes is root's, so "a file not made by
root" was root's and unlinked (the forge's repo-check of 2026-10-09). The owner is now faked as another
account for the refusal and as root for the act; the test fails as before under unshare -r without this.
2026-10-09 17:08:17 +02:00
jschoubben ca07c5ab3b messenger: move to mesh-sdk go/v0.1.10, the tag of mesh-sdk #12's merge 2026-10-09 17:08:17 +02:00
jschoubben 3826741f62 messenger: ask the root-free word fresh before taking an ask
A word kept from a moment ago, not free, refused an ask taken just after root became free again, and
the asker keeps a refusal until something changes. Taking an ask asks the controller fresh, still
before the work is taken (the lab's proof found it).
2026-10-09 17:08:17 +02:00
jschoubben d136806745 messenger: an early desk unlink bars nothing, root lifts any bar, no root-free ask inside the work, no button without its effect (the confirmation review of 2026-10-09)
N1-router: another account's /start offered the desk an unlink of the operator's account in its first
hour, and a desk click, which a program on the desk machine can forge, unlinked it and barred it for a
day, again and again. An unlink at the desk in the first hour now bars nothing: any account, the
operator's too, links again at once with a code from the desk; the day's bar stays for an account
removed by its own approval or by root, and root's allow-link lifts any bar. The words say which.

Lows: the controller's root-free word is asked before the work is taken (it waited up to 10 s inside
it) and the answer is judged with that fresh word; inside the work only a recent word is read, and a
failed ask drops the old one. An answer whose effect the content rule withholds is not offered.
mesh-sdk moves to 16984aa. Each test failed before its fix.
2026-10-09 17:08:17 +02:00
jschoubben ff79defc2b messenger: approve only on the controller's recent word that root is free, and close the review's gaps (hq ADR 0259)
The review of 2026-10-09: the router read the absence of an agent-root condition as a pass, and its minute
hung on a break-glass file, taking the work it already held. An approval now rests on the controller's
verb root-free, asked live for the router's machine and the channel's, failing closed on every other
answer, and a stale word on the kinds promises no verified sender. Break-glass runs in the minute, says
on every channel what root did, and ends the router's own open questions. An ask offering an approval is
answered only where it could be approved; a message says who asks, what each answer does and what expiry
does. The router's own asks bind the account they are about; a link is checked against one per kind as it
is made; an unlinked account does not come back by a code within a day; a fresh link is unlinked from the
desk; no link begins while the desk cannot show its code, and a code is marked secret on every path. Links
are bounded per account in the router's state; a warrant's message id cannot be foreseen. Moves to
mesh-sdk 76902998 (canonical digests).
2026-10-09 17:08:17 +02:00
jschoubben f30089c288 sudo: count any sudo rule as root, fail on what cannot be judged, and commit no binary
The review of 2026-10-09: an unknown account and sudo that could not be asked were answered not root, and
only NOPASSWD on ALL counted, though NOPASSWD on systemctl, tee, bash or pacman reaches root as surely,
and a password the person typed is kept by sudo for minutes. Any rule now counts, as the node-engine's
own judge of an agent account does (ADR 0266); what cannot be judged fails the answer. The bundle built
in the module's directory is removed and ignored.
2026-10-09 17:08:17 +02:00
jschoubben c1d299336b messenger: send an ask's message alone even when only one answer can be given there
The lab's proof showed an ask whose approval was held back (an agent could become root where the
channel runs) folded into a burst's digest with a condition, and its one remaining answer, Decline,
lost with it. A message carrying an ask's answer now always goes alone.
2026-10-09 17:08:17 +02:00
jschoubben 1fc88b9915 messenger: tell the operator, by name and number, when another account answers a question meant for them
The lab's proof showed a stranger's refused tap said as "That answer was not taken" on the operator's
own chat (the router sends to the linked account), where it reads as the operator's own answer refused.
It now says which account tried, that nothing was done, and what it may mean.
2026-10-09 17:08:17 +02:00
jschoubben 8b00ffbfe9 messenger: name the ask's digest in every warrant, and bind the router's own asks to their act (hq ADR 0259 §6)
A warrant now carries the digest of the ask exactly as the router took it, so its asker acts only for
the ask the person was shown, with what each option binds. The router refuses an ask whose approving
option binds nothing (the SDK's Check). Its own unlink and relink asks bind the act they perform, and
it performs that act only on a warrant for that ask. mesh-sdk moves to d4077b4.
2026-10-09 17:08:17 +02:00
jschoubben 4e14ef9835 mesh-delivery: cover a held merge whose module a backlog walk carried to every machine (hq issue 351)
mesh/merge-gate pass: builds mesh-delivery → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
A build the controller held back for a person reaches its machines by a backlog walk on the
operator's word, which credits the delivery that published the build one step per machine and
never the walk's own "every machine running it". Its last record keeps no gate, only the machines
it is done on, so the credited steps stayed judging for ever, and `covered` read only the aggregate
steps, so the held merge had no end but a person's wrong act (the catalogue's #125, held 28 hours
after hq ADR 0270 went live).

A finished backlog walk now passes the steps it carried on the machines it is done on; `carries`
counts a module as sent everywhere when a passed step stands on every machine the held delivery's
plan sends it to; and a walk that carried a step still judging is asked for by its id, since one
that ended before the holder's restart may be older than the ended walks the controller lists
back. Three tests fail before and pass after.
2026-10-09 16:52:37 +02:00
jschoubben f3dcb9dc5a mesh-cli: carry what a pipe or a file gives standard input with the line, at most 64 KiB
mesh/merge-gate pass: builds mesh-cli → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
So a secret reaches the controller's terminal without standing on a command line:
mesh-cli secret accept <node> <module> <name> --from - < file. A terminal is never read, more than the
bound is refused before anything is asked, and nothing carried is printed. mesh-sdk moves to f047d0a.
2026-10-09 16:50:46 +02:00
jschoubben a588eb5983 xdg: keep the folders file trusted, so only the terminal sets the folders (hq issue 346)
mesh/merge-gate pass: builds xdg → g14, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
user-dirs.dirs is shell assignments meant to be sourced by a shell, and a setting is pasted into it as
it is, so a folder of $HOME/x"; <command>; X=" set through the verb would add a command of the
caller's. With the file trusted again, the three folders are set at the controller's terminal alone
until settings are typed and quoted at render.
2026-10-09 12:40:04 +02:00
jschoubben 2d2c694306 betterbird: know a composer by its instance alone, and bound a restart's worst case (review of #155)
mesh/merge-gate pass: builds new: modules/betterbird, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
A composer window whose class is not Betterbird's own was not seen, so a restart could lose its message.
Each step of a restart now has a bound short enough that all of them together, taken in full, stay
under the runtime's 30 s: 3 s for the window tree, 8 s to quit, 3 and 5 s to start, 4 s to see it.
2026-10-09 12:34:06 +02:00
jschoubben ca1bd18de6 picom: keep the configuration trusted, so only the terminal sets the backend (review of #157)
mesh/merge-gate pass: builds picom → g14, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
A setting is pasted into picom.conf as it is, so a backend of xrender"; write-pid-path = "… adds an
option of the caller's: picom would then overwrite any file the operator can write. With the file
trusted, both settings are set at the controller's terminal alone until settings are typed and quoted
(hq issue 346).
2026-10-09 12:33:41 +02:00
jschoubben 73f2962bab picom: the backend and damage tracking are each machine's settings (hq issue 345)
mesh/merge-gate pass: builds picom → g14, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
A window mapped but left unpainted, or stale after a workspace switch, is a known failure of picom's glx
backend with damage tracking on some drivers, and the first suspect for the empty calendar reminder on
the workstation. Research 037 already found the backend to depend on the machine's driver. Both become
preferences whose defaults are today's (glx, use-damage true), so nothing changes until a machine is
given xrender or use-damage false to test it.
2026-10-09 12:25:08 +02:00
jschoubben 4fe8958dda xorg: refuse a window that is not on the screen, and keep an inline screenshot within the bus (review of #154)
mesh/merge-gate pass: builds xorg → g14, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
A window that is unmapped or on a hidden workspace was cut from the screen anyway, which shows what is
there instead of it. An inline image of up to 4 MiB could not cross the bus, whose largest message is
1 MiB; it is now held to 700 KiB and halved in size until it fits, saying so.
2026-10-09 12:23:51 +02:00
jschoubben 3629089ced betterbird: never lose an unsent message, never answer an address, and keep the test apart (review of #155)
mesh/merge-gate pass: builds new: modules/betterbird, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
A restart ended Betterbird with whatever a composer held; now it refuses while a Msgcompose window is
open unless discard_unsent says the operator agreed, and answers within the runtime's 30 s. A
preference's value could carry an address, a user%40domain@host URI, a host or the home; those are
masked, recent-folder, account and cloud-file preferences and every add-on preference but the theme's
id are withheld, and a request for everything is refused. A log kept the token after Bearer or Basic.
The reminder test's Betterbird counted as the operator's and left its profile behind when it ended on
its own; it is now left out everywhere and its unit removes the profile when it stops.
2026-10-09 12:22:47 +02:00
jschoubben 52a4827983 xorg: list the server's windows, and cut a window's screenshot from the screen (hq issue 345)
mesh/merge-gate pass: builds xorg → g14, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
A reminder window that showed as transparent and empty could not be looked at: the window manager's
list leaves dialogs out and says nothing a compositor goes by, and xwd -id refused a window of a
32-bit visual with BadColor (X_QueryColors). xorg_windows answers every window with its class, and
on request its depth, type, state, role and own opacity; a window's screenshot is now cut from the
whole screen as the compositor shows it, and inline answers the image for a session that cannot
read the machine's files. Declares xorg-xwininfo, which both now use.
2026-10-09 12:07:29 +02:00
jschoubben a7953b969d betterbird: a module for the mail and calendar client, so its windows and preferences can be seen and declared (hq issue 345)
mesh/merge-gate pass: builds new: modules/betterbird, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
A calendar reminder showed as a transparent, empty window on shanks and no tool could say which
window it was or which preferences Betterbird ran with. The module owns one file, the user.js its
setting prefs fills, which its tools place whole into the profile Betterbird uses and never over an
operator's own; it reads named preferences without credentials or accounts, restarts Betterbird under
the user manager so its output reaches the journal, checks what it promises, and shows a reminder on
demand from a throw-away profile so a fix can be proved without the operator's calendar. No package:
betterbird-bin is the AUR's.
2026-10-09 12:07:29 +02:00
jschoubben 49f5ba377e The derived-value record is ADR 0202 (was 0201)
mesh/merge-gate pass: builds minio, sent nowhere; no bus step; 1 wait(s) for a person; every machine composes with the change as it did without (4 of 4 com…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
(cherry picked from commit ed10d96048)
2026-10-08 01:42:16 +02:00
jschoubben 69d6b9066f The mesh's one resolver, what every node asks, and a node's hosts file (hq ADR 0194, 0196, 0199)
- dnsmasq holds mesh-dns-resolver: provides wildcard-resolution mesh-wide, forwards every declared
  zone (zones fact), listens on the private address and loopback only, reads no hosts file and no
  operator's files, and no longer writes the container runtime's dns.
- resolv-conf names the mesh's resolver by address, then 1.1.1.1, timeout 1, one attempt; it now
  holds the runtime's live-restore, which dnsmasq held and every node needs.
- resolved-split-dns routes the suffix to the mesh's resolver by address, not 127.0.0.1.
- hosts: new module holding node-hosts-file — the machine's own lines in its block of /etc/hosts,
  the operator's lines kept, changed by entries/add/remove through sudo -n.
2026-10-05 20:37:59 +02:00
jschoubben 1a03caf8a5 The store collects nightly again (hq ADR 0189)
Withdrawn 2026-10-04 to unblock novox: while-stopped named the module-local
id and the host refuses a declaration naming a container it does not have,
whole. mesh-controller#259 fixed the namespacing and it has been live since,
so the window composes as distribution.store and the host will take it.

Unchanged from before: plain garbage-collect at 03:30 with the server held
still. The store is at 40G and nothing reclaims it until this runs.
2026-10-05 14:33:14 +02:00
jschoubben 83bd2afe66 Merge pull request 'restic: restore a single kept file, not only a directory' (#56) from fix/restore-a-file into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 12:22:17 +00:00
jschoubben 46e3a5a6b7 restic: restore a single kept file, not only a directory
The first restore of an arr app refused its settings file with "not a directory": restic restores
a snapshot's subfolder, not a file. A file is restored with its full path into a scratch directory
beside the target, moved into place, and the scratch removed.
2026-10-05 14:05:52 +02:00
jschoubben d67e786bf9 Merge pull request 'restic: declare sqlite, the tool SQLite stores are copied with' (#55) from feat/arr-backups into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 11:49:35 +00:00
jschoubben 7786507d45 restic: declare sqlite, the tool SQLite stores are copied with
The arr apps keep SQLite databases; a consistent copy of a live one is sqlite3's .backup. Declared
once, by the holder that runs the copies, rather than by each app.
2026-10-05 13:39:55 +02:00
jschoubben a70227a40a Merge pull request 'restic: ask as root whether a directory is there' (#54) from fix/backup-sees-as-root into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 10:38:25 +00:00
jschoubben 08c7a79f79 restic: ask as root whether a directory is there
The first run on the control node called postgres's dumps missing: the holder looked as the
runtime's account, which cannot see inside a store's 0700 data directory. Every other act already
runs as root; the existence checks do too now.
2026-10-05 12:31:07 +02:00
jschoubben d9120c6848 Merge pull request 'fail2ban: its tools in Go' (#53) from feat/fail2ban-in-go into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 10:24:17 +00:00
jschoubben 96ee0d7ad6 Merge pull request 'Back up every store: the restic module and the stores' contributions (hq ADR 0214, to-be 43)' (#49) from feat/node-backup into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-05 10:13:48 +00:00
jschoubben d43de93e49 fail2ban: its tools in Go
Go is the default for module code. One binary, fail2ban-tools, serving the node-intrusion-prevention
seat's four verbs and fail2ban_settings over the SDK, with the same parsing and the same tests; read
back against the control node's live daemon.
2026-10-05 12:01:25 +02:00
jschoubben 3a88bca21a restic: the holder in Go
Go is the default for new module code; the holder is one binary like the licence manager, serving
the seat's verbs over the SDK and running the nights beside them. Same behaviour and tests.
2026-10-05 11:58:18 +02:00
jschoubben 32cd92baeb Back up every store: the restic module holds node-backup, the stores contribute their dumps
ADR 0214 / to-be 43. restic keeps one repository per machine and takes a nightly snapshot per
module — 14 daily, 8 weekly, 6 monthly — and restores beside the live data, never over it. postgres,
mssql and mongodb contribute a consistent dump; minio, influxdb, the vault, mailu, gitea and
nextcloud the directories that hold their data.
2026-10-05 11:47:59 +02:00
jschoubben 316576f1da Merge pull request 'A withdrawn consumer keeps its data, in every provider that holds some (hq issue 241)' (#44) from fix/a-withdrawn-consumer-keeps-its-data into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 23:45:16 +00:00
jschoubben 1fb7ca3d72 A withdrawn consumer keeps its data, in every provider that holds some (hq issue 241)
mssql disables the login, mongodb takes the user's roles, minio revokes the key and keeps the bucket,
mailu disables the mailbox, gitea prohibits the login instead of purging the user and their
repositories, umami keeps the website. Each provider's create already enables what this locks.
2026-10-05 00:34:40 +02:00
jschoubben 190d711a2a postgres: a withdrawn consumer keeps its database; retiring renames, never drops (hq issue 241) 2026-10-05 00:33:12 +02:00
jschoubben 7aaf784bb5 Merge pull request 'Remove the catalogue's photos: the app's own repository defines it' (#278) from fix/photos-lives-in-its-own-repository into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-04 15:05:31 +00:00
jschoubben 4004dee825 Remove the catalogue's photos: the app's own repository defines it
Two definitions held one module name. Rebuilt to this catalogue's main on 2026-10-04, the mesh took
this stub — a server and an admin client — over the app's definition in photos.git, and five of its
six sites lost their routes. The app's repository is the source, as de-spiegel's and link2pay's are.
2026-10-04 16:35:28 +02:00
jschoubben 5d59b35cf7 photos authenticates against the database its user lives in (hq issue 232)
The provider creates each consumer's user in that consumer's own database.
photos asked for admin, where no such user exists; invoicing, against the
same provider, already asked for the name the mesh gave it and worked.

Invisible until hq 225 was fixed: while the provisioner could not read its
secrets, no user existed anywhere, so 'UserNotFound for db admin' was a true
and complete account of that fault. A fault that explains the symptom is not
evidence there is only one.
2026-10-04 12:33:56 +02:00
jschoubben 1bc6daf31b The photo clients publish the endpoint they declare (hq issue 227)
Each declares a web endpoint — 4001, 4012, 4013 — and published a bare 80,
which the mesh has nothing to assign for, so 80 reached the machine and
collided with the reverse proxy. Written the long way, the software's 80 is
published at the port the module declares and the mesh rewrites the outer
half to whatever it assigned.

photos is the one that failed on the control node; the other two are the same
fault waiting for a machine that runs a proxy.
2026-10-04 12:25:28 +02:00
jschoubben 525c639041 The store does not collect until every controller composes the window (hq ADR 0189)
mesh-controller#259 fixes while-stopped to name the container as the machine
knows it — `distribution.store`, not `store`. Until that controller is the
one composing, novox refuses its whole declaration and takes nothing at all.

The step comes out; deletion stays on, already applied and harmless on its
own. A collect step without its window would be worse than none: garbage
collection against a live registry can sweep a blob a build is pushing.

Put back once the fixed controller is deployed and stays.
2026-10-04 04:26:38 +02:00
jschoubben 159ed53103 Rebased onto main: ADR 0188 renumbered to 0201, and minio takes the sdk at 0.1.7
The bundles refactor took ADR 0188 on main, so minio's comments cite 0201.
The sdk is 0.1.7 after the same rebase, and minio needs the `derived` field
it carries.
2026-10-04 02:45:13 +02:00
jschoubben 723e676b75 The store enables deletion and collects nightly (hq ADR 0189)
REGISTRY_STORAGE_DELETE_ENABLED on the server — the door already accepts a
push — and a scheduled step running the registry's own collector over the
volume at 03:30 with the server held still. Plain garbage-collect: what the
mesh keeps is still a manifest, so --delete-untagged is not needed and would
delete images machines are running.
2026-10-04 02:34:06 +02:00
jschoubben 661114370f minio declares the bucket it derives; its consumers stop transcribing it (hq ADR 0188)
serves.s3-bucket.bucket is ${consumer:as:dns}; the provisioner uses what it
is given. nextcloud, invoicing and photos ask for ${bound:s3-bucket:bucket}
instead of naming mesh-novox-* literals, which also named this node.
bucketFor and the long-dead accessKeyFor are gone.
2026-10-04 02:34:06 +02:00
jschoubben 0e8ad3a8e1 Merge pull request 'dnsmasq: restart on the mesh hosts region, which it reads only at start' (#241) from fix/dnsmasq-rereads-the-names-region into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 13:29:29 +00:00
jschoubben b43e405947 dnsmasq: restart on the mesh's hosts region, which it reads only at start
The names region of /etc/hosts is written by mesh-wireguard and dnsmasq answers from it, but read
it once at start: after the mesh stopped publishing public names (hq ADR 0191) every machine's
hosts file was right and every resolver still answered mail.novox.be with a tunnel address. The
config comment that says so changes the file, which restarts dnsmasq once everywhere.
2026-10-03 15:29:28 +02:00
jschoubben 38be3ba7f0 Merge pull request 'ssh-client: own ~/.ssh, not the openssh package' (#238) from fix/ssh-client-owns-no-package into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 10:43:46 +00:00
jschoubben a19638d112 ssh-client: own ~/.ssh, not the openssh package
A package has one owning module, and sshd already owns openssh on every node — assigning
ssh-client was refused for both declaring it, and the refusal left every node unresolvable
until it was unassigned. Owning it was wrong besides: unassigning ssh-client would have
removed the package sshd serves from. The client binary ships in the package sshd holds.
2026-10-03 12:43:35 +02:00
jschoubben 1cbddeb104 Merge pull request 'ssh-client module: the mesh owns ~/.ssh, config from the hub (to-be 29)' (#237) from feat/ssh-client-module into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 10:40:13 +00:00
jschoubben 301aeda5f3 ssh-client module: the mesh owns ~/.ssh, config from the hub (to-be 29)
Requires openssh; creates ~/.ssh (0700, owned by the operator account via
${machine:account}); writes every other node's Host block (HostName + User
<account>) into a marked region of ~/.ssh/config (home-scoped, into:block), so
`ssh <node>` reaches each peer as the right account and the operator's own
config is kept. Universal-tier: assigned wherever a person logs in; a node with
no account gets no config.
2026-10-03 11:58:05 +02:00
jschoubben 85af10ebad Merge pull request 'step-ca: stop offering acme-ca, so public names are certified by public-acme' (#234) from fix/step-ca-is-not-the-public-acme-ca into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on
2026-10-03 08:57:25 +00:00
jschoubben 0778f8f0ae step-ca: stop offering acme-ca, so public names are certified by public-acme
step-ca and public-acme both offered acme-ca on novox, and route-proxy's pin names a node, not
a module — so which one certified the public names depended on provider order. After the
controller restart on 2026-10-03 it came out as step-ca, and every public site served a
certificate no browser trusts. step-ca's own names are already certified through
internal-acme-ca; acme-ca is the public authority's alone.
2026-10-03 10:54:43 +02:00
jschoubben a309deb479 Remove portainer: deprecated, and unassigned everywhere
It held the docker socket behind a public name. Nothing depends on it;
it is off both machines that ran it, with its data.
2026-10-02 23:25:45 +02:00
jschoubben 2e6cc71f7a gitea: the jail also bans what gitea's sshd refuses
The jail read gitea's container journal, which carries its sshd's lines,
but matched only the web login. 167 ssh attempts an hour from the
internet went unbanned. Two patterns, one per attempt: an unknown user,
and a user sshd refuses; tested against a day of the real log, 946
matches and none on an accepted login.
2026-10-02 23:23:35 +02:00
jschoubben 304044da40 A ban list never holds a neighbour (hq ADR 0186)
The home server banned the house's own router within an hour of the first public jail: the router
reflects local traffic, so every client in the building arrives as the gateway's address. Every
private range joins the mesh's own in the never-ban list.
2026-10-02 18:42:16 +02:00
jschoubben 23112b111c The proxy's jail reads both refusals, each pattern naming the host once
fail2ban expands <HOST> to a named group, so two in one pattern is a duplicate group name and
the daemon refuses to start at all -- every jail on the machine, not just this one. Two patterns,
one <HOST> each: the certificate refused for an unserved name, and the request refused for one.
Caught live on the control node (hq ADR 0179).
2026-10-02 17:23:42 +02:00
jschoubben b547308e05 The proxy's jail reads a refused name as well as a refused certificate
The pattern ended at the line's end, which only the certificate refusal does; a request for
an unserved name carries trailing text and never matched. Caught against the live lines
before the jail counted anything (hq ADR 0179).
2026-10-02 17:20:54 +02:00
jschoubben 1601d5a335 fail2ban holds the intrusion seat's verbs and composes the jails; mail, forge and proxy declare theirs (hq ADR 0179, to-be 31)
The module gains a runtime carrying only the fail2ban client with the daemon's socket shared in,
serving status/banned/ban/unban and its own fail2ban_settings. It declares jailing, so the
controller's composition lands in jail.d/mesh.conf and filter.d; mailu, route-proxy and gitea log to
the journal and declare a jail reading it by container name. The base is strict: three in a day for
a day, twice banned in two weeks for four; the mesh's range stays never banned.
2026-10-02 17:02:49 +02:00
jschoubben 3dfbad6f03 nftables declares the ufw front end absent once its filter is loaded (hq ADR 0175) 2026-10-02 16:27:34 +02:00
jschoubben 6dfd2401c9 lab: the image carries what the builds and the lab call: python3, file, iproute2, sudo 2026-10-02 15:23:52 +02:00
jschoubben 8cd4f199f1 lab: a run resolves the @novox scope from the forge's package registry 2026-10-02 15:13:22 +02:00
jschoubben bce7b3a551 lab: compile from the module's root, so the runtime finds its tools
Both sources sit in tools/, so tsc took tools/ as the root and wrote
dist/index.js, while the runtime loads dist/tools/index.js: the module
started and served nothing.
2026-10-02 15:08:01 +02:00
jschoubben b77582f6a6 Cite hq ADR 0170, not 0169: the firewall seat's record was renumbered after a collision on hq main 2026-10-02 14:52:26 +02:00
jschoubben b3865d240f lab: declares the virtualisation capability, which grants its daemon's socket 2026-10-02 14:48:10 +02:00
jschoubben 67d1a400e8 lab: the lab as a module, running beds when the mesh asks
Five tools on the machine the lab runs on: check, run beds against
branches on the forge, a run's status, its log, and stop. A run checks
out every repository the lab builds, side by side, and runs the suite;
one at a time, answered at once with an id (novox/hq ADR 0172).
2026-10-02 14:15:42 +02:00
jschoubben 663e8143d4 nftables holds the node-packet-filter seat: rules, reload and remove, from a runtime with NET_ADMIN (hq ADR 0169)
The seat's three verbs over the machine's own tools: the filter as enforced
(nftables and the legacy filter), the mesh's own table reloaded from its file,
and one rule set the mesh did not write removed by the name the host reports
it under (ADR 0168) — a predecessor's chain loses its jumps and goes, the
runtime's user chain is emptied back to its return, a table of the machine's
own goes whole; the mesh's tables, the runtime's chains, a built-in chain and
an active found firewall's chains are refused. Tested over the shapes two
machines of the first mesh reported live. The module's own tool stays.
2026-10-02 13:28:33 +02:00
jschoubben d1f8ab86d1 unifi: list networks and set the DNS their DHCP hands out
Which DNS server the home network's DHCP hands out could be changed only
in the controller's own interface or by hand against its API (novox/hq
issue 198).
2026-10-02 11:53:16 +02:00
jschoubben b72213261a dnsmasq: listen addresses are a setting, and docker's file takes none
The addresses dnsmasq listens on beside the machine's are a setting, so a
machine that answers its own LAN can say so (novox/hq issue 198). Docker's
daemon.json no longer merges the module's settings: it needs none, and a
setting reaching it is a key dockerd refuses. The host still merges it
into the existing file.
2026-10-02 11:49:57 +02:00
jschoubben 67f5236b01 Revert dnsmasq's listen addresses as a setting
A module's settings merge into every mergeable file it owns, so the
setting reached docker's daemon.json beside dnsmasq's config, where
dockerd would refuse it (novox/hq issue 198). Back to the fixed
loopback line until settings can be kept out of files they are not for.
2026-10-02 11:48:37 +02:00
jschoubben 56a22847f5 dnsmasq: the addresses it listens on beside the machine's are a setting
Loopback by default, as before. A machine that answers its own LAN adds
its LAN address, and its DNS endpoints' reach opens the filter (novox/hq
issue 198). The mesh-wide default must be set before this lands.
2026-10-02 11:42:41 +02:00
jschoubben 4f952ce771 route-proxy: a bus account, to read its membership
The proxy reads its routes and the mesh's addresses from its membership
on the bus rather than from a file alone (novox/hq ADR 0167, issue 191).
2026-10-02 01:46:18 +02:00
jschoubben 400b2f9696 mssql: the query runs as a read-only login, one line, no variables; sqlcmd is installed (hq #193)
Proven on a throwaway server: as the administrator a caller's $(SQLCMDPASSWORD) returned
the sa password, and a line beginning ':!!' ran a program in the tools container. The
statement now runs as mesh_mssql_reader (CONNECT ANY DATABASE, SELECT ALL USER SECURABLES),
with substitution off (-x), after the module's own text on the first line, and a line break
is refused. go-sqlcmd v1.10.0 is installed at a pinned digest: the image never had sqlcmd,
so every mssql tool failed with spawn sqlcmd ENOENT.
2026-10-02 00:25:22 +02:00
jschoubben 160b5ad65a postgres: the store's query runs as a read-only login, never as the admin (hq #193)
The verb wrapped the caller's text in BEGIN READ ONLY ... ROLLBACK as the superuser, so
'COMMIT; ...' left the transaction and, proven on a throwaway server, COPY TO PROGRAM ran a
shell command on the database host. The statement now runs as mesh_store_reader:
pg_read_all_data, no other grant, read-only transactions by role and session, its password
an own-secret the mesh mints. Without that password the call is refused. -q drops the
command tags that came back as rows keyed by BEGIN.
2026-10-02 00:09:09 +02:00
jschoubben 0651b63926 route-proxy README: the node that runs a proxy carries public-acme (hq #258) 2026-10-01 17:31:58 +02:00
jschoubben 01d68bda88 And the uplink holders' capabilities return
The same split lost them the other way round: the merge base held both changes, each branch had reset
the other's files, and the three-way merge kept neither. Both halves of hq ADR 0161 are on main again
with this.
2026-10-01 17:19:41 +02:00
jschoubben 89e0dde9e0 The vault's claim and its own event names return
The uplink branch was split from the vault's with the vault's files reset to a main that did not yet
hold #205; merging it afterwards took the older vault definition along (no claim, the refused event
names), and the vault could not be built. Restored to #205's state.
2026-10-01 17:19:05 +02:00
jschoubben 2e96d2f67d This branch carries the uplink capabilities alone (hq ADR 0161 rule 3); merges once every machine running a holder has reported uplink-<manager> 2026-10-01 16:52:47 +02:00
jschoubben 932efb5186 This branch carries the vault's claim alone; the uplink capabilities wait for every machine to report its profile 2026-10-01 16:52:46 +02:00
jschoubben 6ba61a8b4b The provisioner announces the vault's events by their new names 2026-10-01 16:51:13 +02:00
jschoubben 8368697744 The vault's events are its own: provisioned, rotated, deprovisioned
A module publishes under its own name only; secret.provisioned read as another module's event and the
builder refused the vault's definition today, so the seat claim could not be built. The three events lose
the prefix; nothing outside the vault listens for the old names.
2026-10-01 16:50:56 +02:00
jschoubben 966fed1829 The vault claims mesh-vault, and each uplink holder declares the manager it speaks for (hq ADR 0161)
The vault's claim makes a second provider of secret a second claimant, refused by name. The three
uplink definitions declare uplink-networkmanager, uplink-systemd-networkd and uplink-dhcpcd, which the
host reports for the manager it finds active, so the holder for a manager the machine does not run
is refused the way any missing capability is. Merges after the controller holds the seat and the host
reports the capability.
2026-10-01 15:58:05 +02:00
jschoubben c00dd04494 postgres implements the store seat's verbs under the seat's name, and its claim says so (hq ADR 0160)
The store's databases and query are registered under mesh-store, so the runtime serves them on the
seat's subjects wherever postgres holds the seat and never lists them as postgres's own; the claim
names them, so the mesh can judge the holder without postgres listing the seat's verbs among its
tools. Scoped to what the store enables: creating a database stays postgres's tool.
2026-10-01 15:19:35 +02:00
jschoubben abf5859415 Merge remote-tracking branch 'origin/main' into feat/postgres-serves-the-stores-verbs 2026-10-01 15:19:08 +02:00
jschoubben 738415710c The media chain's stale copies leave the catalogue
bazarr, bookshelf, lidarr, nzbget, ombi, plex, qbittorrent, radarr,
sonarr and tautulli live in novox/mesh-media-catalog (#195 moved jackett
and left these behind). A build of this repository at a commit today
registered plex and nzbget from these copies, which carry no provides,
and ace's plan stopped resolving. Nothing here depends on the directories:
home-assistant consumes their provisions by name.
2026-10-01 14:53:35 +02:00
jschoubben 4c7e438ea3 postgres serves the store seat's verbs, databases and query, and lists its tools (hq ADR 0159)
Named as the seat names them so the runtime finds them by name; the same calls as its own tools.
Its definition now lists its tools, which is what holding a seat with verbs demands at registration.
Merge before the controller declares the verbs on the mesh-store seat.
2026-10-01 14:02:41 +02:00
jschoubben 50c08818d6 searxng says it reads its secret key at start, so the mesh may rotate it (hq 180)
The key signs sessions and nothing else holds it; it lands in the settings file the server
restarts on, so a rotation is a new value and a restart.
2026-10-01 12:09:47 +02:00
jschoubben 6b0164c2ba nodered says it reads its API token and admin password at start, so the mesh may rotate them (hq 180)
Both land in settings.js and the runtime's config file, and the containers that read them restart
on those files; a rotation is a new value and a restart. The broker credential says nothing yet: its
other party is the bus, and that rotation is the two-party form.
2026-10-01 11:44:29 +02:00
jschoubben 171f8a03f6 The forge's tools close and read pull requests, read files and branches, and delete a branch
Ten tools the console lacked for the actions a review and a merge leave behind: close or reopen a
pull request whose work landed elsewhere, change its title or body, read its files, its diff and its
comments, reopen an issue, read one file at a ref, list branches, delete the branch a closed pull
request leaves. Each is the client's own call; `gitea_api` stays the escape hatch for the rest.
Tested against the fake forge through the compiled tools, the way the console calls them (13/13).
2026-10-01 11:25:34 +02:00
jschoubben 9fc0d675cd n8n: its media library is an access placed by the assignment
The container mounted /services/media literally — one installation's path
(ADR 0112). The access is now declared by id and mounted as ${access:media};
the assignment says where the library is (ace: /storage/media, hq 153).
2026-10-01 02:02:30 +02:00
jschoubben 5c4364e462 n8n: its own image built from source, placed data, and what its workflows use
The module named /var/lib/n8n, /services/n8n/n8n-data and n8n.novox.be -
paths and a domain no definition may carry (ADR 0112). State and data are
placed directories; the public name is ${bound:route:name} (depends on
mesh-controller #149), for N8N_HOST and WEBHOOK_URL alike.

The endpoint said 5682 while the container publishes 5678. 5682 was one
machine's host port; the endpoint is the software's port and the mesh
assigns the machine's (ADR 0038).

n8n had been run from an image in a registry that no longer exists: the
upstream image plus shadow, a `media` group (2000) with `node` in it, and
a global `uuid`. That recipe is now this module's Dockerfile, built on the
upstream 1.71.3 image named in build.on by digest, with uuid pinned to the
version the running image carries (14.0.1) - Code nodes require() it. The
media group is how the container writes into the shared media library, a
read-write `access` (ADR 0051), mounted where workflows expect it,
/media-library.

The workflows also use a redis (the Redis nodes of the chat workflows) and a
Selenium Chrome (the scraper), which the previous deployment ran beside n8n.
Both are containers on the module's own network, publishing nothing, pinned
to the digests in use; redis keeps its append-only file in a placed
directory.

The basic-auth secret is gone: N8N_BASIC_AUTH_* was removed in n8n 1.0 and
did nothing. The grant's password is a 0400 file owned by `node`, read
through DB_POSTGRESDB_PASSWORD_FILE, so nothing secret is in the
environment. The credentials' encryption key is n8n's own, in the data
directory (config), and moves with it - nothing to mint or accept.

Verified: catalogue tests with MESH_CATALOGUE set; the Dockerfile built
against the pinned base gives n8n 1.71.3, uid 1000 in group 2000, uuid
14.0.1 - the running image's shape. Throwaway containers: an instance on
PostgreSQL 15 with an owner, a workflow and an encrypted credential;
stopped, copied, dumped from the copy, restored (--no-owner --role, the
uuid-ossp extension pre-made by the superuser) into a grant-shaped
database on the postgres module's pgvector image (PG17); the new shape
(password from the file, data dir copied) serves /healthz, the owner logs
in, the workflow is listed, and the credential decrypts with the carried
key. The node user writes into a root:2000 0775 library through the media
group; redis and Selenium resolve by name on the module network and
Selenium reports ready. Test containers and data removed.
2026-10-01 01:52:23 +02:00
jschoubben 684b9853ad An access has an id, and its mounts name it (issue 153)
Ten definitions name each access by id; the path stays as the default an assignment may replace,
and the host side of every mount says ${access:<id>}. Resolved with no placement, every definition
names exactly the paths it named before (TestPlacedDirectoriesKeepTheirPaths, extended). On an
adopted machine the assignment now says `accesses: {<id>: <path>}` and the mount follows.

Needs the controller that knows an access id (mesh-controller #176); the running one refuses the
field at registration.
2026-10-01 00:01:42 +02:00