Compare commits

..
Author SHA1 Message Date
jschoubben e53ac973e9 mesh-delivery-gate: stop asking the controller's forget-checks (issue 1318, the gate's part)
The controller's forget-checks goes with the merge gate's checks (ADR 0503 §3)
and 1318 removes the verb in the same group, so the gate must stop calling it
first: a closed pull request is stopped and asks the controller nothing, and
the manifest no longer invokes it. delivery-check stays for hq's base until
R4g (the lead's decision for 1302).
2026-10-22 17:50:44 +02:00
jschoubben f1f18bf2bd mesh-delivery-gate: read mesh/check alone on a base that requires it (issue 1302, S1-S10)
Once main requires mesh/check, the controller publishes no verdict for it and
the gate, which moved deliveries on the controller's checked, would hold every
pull request proposed. A head's mesh/check is now its verdict, from the
forge's status event (gitea now sends the status's words) or its open pull
requests. The Approve is asked only on the checker's verdict at exactly the
heads the ask covers, so a member judged alone never passes its group; a
group on mesh/check asks no composed check and owes no mesh/merge-gate; a
member's mesh/check that changes withdraws the ask. Nothing is asked of the
controller for such a base, and recheck refuses, naming checker_pull. hq's
mesh/repo-check keeps today's path.
2026-10-22 17:50:25 +02:00
jschoubben c41c968086 gitea: judge and write only the statuses a base requires, so merges go on mesh/check (issue 1301, S5-S7)
Once main requires mesh/check, nothing writes mesh/merge-gate, and the merge
tool, which judged on it, would refuse every merge. It now judges on the
status the base requires: mesh/check's line names the trunk it judged, an
out-of-date one is refused and nothing is asked (the checker re-checks on a
trunk move, ADR 0447 §4); hq's mesh/repo-check is judged as before. The
announcer, the verdict, a trunk's move and a build's start write the
controller's two statuses only where a base still requires them, so hq keeps
mesh/repo-check and no stale mesh/merge-gate appears anywhere else.
2026-10-22 17:49:53 +02:00
jschoubben 939475cb63 gitea: only the forge bridge writes mesh/check, through a tool of its own (issue 1301, S4)
Once main requires mesh/check, a hand that set it could pass a merge past the
checker, the gap issue 293 closed for the two statuses before it.
gitea_commit_status now refuses mesh/check in every state, and the new
gitea_check_status writes it alone; the bus grants that tool only to a module
whose manifest invokes it, the forge bridge (the lead's choice A).
2026-10-22 17:49:53 +02:00
jschoubben 5e24deb0f1 gitea: require mesh/check in place of the two old statuses on every main the gate judges (issue 1301, S1-S3)
ADR 0447 §1 has main require mesh/check alone. The repositories program now
requires it in place of mesh/merge-gate and mesh/repo-check on every listed
repository and on every other one whose main requires mesh/merge-gate, so the
switch needs no hand-kept list (lead decisions L4, L11). A main requiring
mesh/repo-check alone (hq) keeps it until issue 1296 (ADR 0503 §5).
2026-10-22 17:49:53 +02:00
mesh-tools 49caf17ff5 Merge pull request 'only-office: keep its working directories under a root-only box (issue 1843)' (#448) from fix/1843-private-box into main 2026-10-22 15:33:32 +00:00
mesh-tools cac09a4e20 Merge pull request 'Ask a check the build queue lost again, and take a recheck from proposed when no check of its head is held (issue 1274)' (#447) from fix/1274-ask-a-lost-check-again into main 2026-10-22 15:27:42 +00:00
jochen 29b4c02709 only-office: keep its working directories under a root-only box (issue 1843)
mesh/check checking 29b4c0270987 on cac09a4e203e
mesh/merge-gate head 29b4c0270987 merged into trunk cac09a4e203e — pass: builds only-office → novox; no bus step; every machine composes with the change as…
mesh/repo-check head 29b4c0270987 merged into trunk cac09a4e203e — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The image opens everything below each mount to every account on every
start, so with the module root and lib at 0711 any account on the
machine could read the document cache. The mounts now sit under a
root-owned 0700 box the container never sees; the root stays 0711 for
the converter. The old lib keeps its id so mesh-host closes it to 0700
(a new id there would be used as found, issue 339).
2026-10-22 17:24:05 +02:00
mesh-tools c3d3194bc8 Merge pull request 'nextcloud: keep onlyoffice's jwt off argv, and re-check a failed connection every tick (issues 1831, 1832)' (#446) from fix/onlyoffice-followups into main 2026-10-22 14:51:59 +00:00
mesh-tools d0463fb510 Merge pull request 'Fetch a Go pin only a pull request carries from its pull head, so a group member pinning its mesh-sdk head builds (issue 1148)' (#445) from fix/1148-pull-head-pins into main 2026-10-22 14:40:56 +00:00
jochen 6bf054b982 Take issue 1843's owner change back out: giving the converter the module root opens a link swap to root (review), so 1843 needs its own design
mesh/check failed: unit: modules/gitea.test: ✔ the same merge, head or close announced twice has the same id, and another has another (8.058792ms) | 6…
mesh/merge-gate head 6bf054b98261 merged into trunk d0463fb5100f — pass: builds nextcloud → novox; no bus step; every machine composes with the change as i…
mesh/repo-check head 6bf054b98261 merged into trunk d0463fb5100f — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-22 16:37:35 +02:00
jschoubben fbbf28d21c mesh-delivery-gate: read the queue's state word as the controller says it, in flight (issue 1274)
mesh/check failed: unit: modules/gitea.test: [gitea] where the poll of merges last looked from could not be read, so none is kept — no runtime | fbbf2…
mesh/merge-gate head fbbf28d21c37 merged into trunk c3d3194bc8d4 — pass: builds mesh-delivery-gate → novox; no bus step; every machine composes with the ch…
mesh/repo-check head fbbf28d21c37 merged into trunk c3d3194bc8d4 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-22 16:36:23 +02:00
jschoubben fb89990e2b mesh-delivery-gate: ask a check the build queue lost again, and take a recheck from proposed when none is held (issue 1274)
A proposed delivery whose asked check no queue entry holds stayed proposed for good (met in 1311, 1336, 1726, 1792, 1795, 1833). The sweep reads the controller's queue and asks again past max(10 minutes, the repository's last check time); recheck from proposed is taken on the same fact; never while the queue holds one.
2026-10-22 16:34:33 +02:00
jochen d0a3784be2 only-office: bar every account but the converter from the state root (issue 1843)
lib (bind-mounted into the container at /var/lib/onlyoffice) was 0711 so
nginx inside the container could reach App_Data/cache, and the module
root above it stayed 0711 too, so any account on the machine could walk
through the root into lib's 0755 directories and 0644 files to read
converted documents. The container never passes through the root — only
the converter process does, to read jwt.secret and
broker-converter.secret — so the root is now 0710, owned by the
converter account: only that account and root pass through it. lib
stays 0711 (1837/AC3).
2026-10-22 16:34:06 +02:00
jschoubben 555ed4963a Read the pinned placement at its new path in the pin integration test (issue 1148)
mesh/check failed: unit: modules/gitea.test: [gitea] where the poll of merges last looked from could not be read, so none is kept — no runtime | 555ed…
mesh/merge-gate head 555ed4963a8a merged into trunk 77578a882f8a — pass: builds mesh-build-go → ace, g14, novox, shanks; no bus step; every machine compose…
mesh/repo-check head 555ed4963a8a merged into trunk 77578a882f8a — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-22 16:29:37 +02:00
jochen edb6d3b7d3 nextcloud: keep checking onlyoffice's connection while the last check failed (issue 1832)
applyOnlyOffice only ran the connection check when a setting changed, so
once it failed while settings matched, nothing checked again and the
failure was logged once and forgotten. The client now remembers whether
the last check failed and re-runs it on every tick until it passes;
index.ts logs every failing tick and one line when it recovers.
2026-10-22 16:28:35 +02:00
jochen 8928e6f972 nextcloud: keep onlyoffice's jwt_secret off every argv (issue 1831)
occ's config:app:set put the secret on the docker exec command line, so
it sat in the docker CLI's argv, the container's php argv and docker's
exec record. Apply jwt_secret through config:import on occ's stdin
instead; the other three onlyoffice keys still go through
config:app:set.
2026-10-22 16:27:15 +02:00
jschoubben 2d8c8f6d75 Share one placement per repository and commit for pins on pull heads; say a pin is on no pull request head and what go said (issue 1148) 2026-10-22 16:24:18 +02:00
jschoubben 9194e15072 Fetch a Go pin on a pull request's head: place it beside the workspace and hand it to Go by a modfile replace (issue 1148) 2026-10-22 16:19:23 +02:00
mesh-tools 77578a882f Merge pull request 'Hold a delivery's problem messages until it settles, and say a refusal only when its fresh check did not cure it (ADR 0505)' (#444) from feat/quiet-deliveries into main 2026-10-22 14:01:19 +00:00
mesh-tools 3f9081d00e Merge pull request 'Let nginx through only-office's lib directory, so OnlyOffice serves converted documents instead of answering 403 (issue 1837)' (#443) from fix/1837-lib-traverse into main 2026-10-22 13:44:06 +00:00
mesh-tools 7842001c53 Merge pull request 'A tool verifies the bus's snapshot, so an upgrade or a restore is checked without a docker exec (issue 1842)' (#442) from fix/1842-bus-snapshot-verify into main 2026-10-22 13:08:47 +00:00
jschoubben 09e749bc1a Format the ADR 0505 test file, so the repository check passes
mesh/repo-check head 09e749bc1a1c merged into trunk 3f9081d00eb8 beside mesh-controller#436@b7d03b68 — pass: its merge-check.sh passed, each part in its to…
mesh/check failed: g14: novox/mesh-catalog#444: checks/particulars/go.mod cannot be read to check its mesh-sdk pin: Gitea API /repos/novox/mesh-catalo…
mesh/merge-gate head 09e749bc1a1c merged into trunk 3f9081d00eb8 — pass: group feat/quiet-deliveries composed: every machine composes with the change as it…
mesh/delivery delivered
mesh/delivery-group group feat/quiet-deliveries delivered: every member is delivered
2026-10-22 15:00:37 +02:00
jschoubben ea8692b08f Say a message two walks held once, in the message of the walk whose limit is due (ADR 0505 §7) 2026-10-22 14:39:35 +02:00
jschoubben 3cf75580b5 Never hold a message past its first hold's limit, so a fault on a machine walk after walk touches still reaches the operator (ADR 0505 §7) 2026-10-22 14:36:35 +02:00
jochen a22fdd015b Let nginx through only-office's lib directory, so it can serve the converted files the document service writes there instead of answering 403 (issue 1837)
mesh/check failed: unit: i3/cmd/i3-tools.TestTheMainFileAndEveryModulesContributionLoadTogether: manifest_test.go:247: expected the launcher, clipboar…
mesh/merge-gate head a22fdd015b09 merged into trunk 7842001c53f7 — pass: builds only-office → novox; no bus step; every machine composes with the change as…
mesh/repo-check head a22fdd015b09 merged into trunk 7842001c53f7 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-22 14:32:06 +02:00
mesh-tools dcf6e86d02 Merge pull request 'Refuse a node name of the mesh in the pacman and kernel modules (issue 1838)' (#441) from fix/1838-particulars into main 2026-10-22 12:24:35 +00:00
jschoubben 98ca97fc5b Verify the bus's snapshot with a tool, sharing the snapshot program's archive code (issue 1842)
mesh/delivery delivered
mesh/check failed: ace: novox/mesh-catalog#442: checks/particulars/go.mod cannot be read to check its mesh-sdk pin: Gitea API /repos/novox/mesh-catalo…
mesh/merge-gate head 98ca97fc5b3f merged into trunk dcf6e86d0236 — pass: builds nats, sent nowhere; merging this needs a person's bus upgrade; 1 wait(s) fo…
mesh/repo-check head 98ca97fc5b3f merged into trunk dcf6e86d0236 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
The upgrade and restore steps told a person to docker exec the snapshot program; no mesh tool
did it. nats_snapshot_verify reads the newest bus.tar of the snapshots directory only.
2026-10-22 14:24:25 +02:00
mesh-tools db67a0e55e Merge pull request 'Pin only-office's secure-link secret, so OnlyOffice's signed download links verify across restarts (issue 1837)' (#440) from fix/1837-secure-link into main 2026-10-22 12:20:22 +00:00
jschoubben 34277ef60f Prove over the real bus that a refusal the gate answers is stalled only once its fresh check did not cure it (ADR 0505 §4) 2026-10-22 14:18:33 +02:00
jschoubben a3b51a9d9a issue 1838: catch node names in paths, hyphenated and lowerCamel forms; test the exit codes
mesh/merge-gate head a3b51a9d9a02 merged into trunk db67a0e55ec3 — pass: the change touches no module of the mesh's graph
mesh/repo-check head a3b51a9d9a02 merged into trunk db67a0e55ec3 — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/check failed: unit: nfsserver/cmd/nfs-server.TestARealRootOwnedFolderIsRefused: --- FAIL: TestARealRootOwnedFolderIsRefused (0.00s) | a3b51a9d9a0…
mesh/delivery delivered
The forge blank hid any novox/ path segment, and joined names passed; read covered from the root.
2026-10-22 14:18:26 +02:00
jschoubben e5608f7b00 Say in the gate's README when a refused or rejected merge reaches the operator (ADR 0505 §4, §5) 2026-10-22 14:17:40 +02:00
jschoubben 5a18eb5f4a Leave a delivery that needs a new head to its author for two hours, then name the author to the operator (ADR 0505 §5) 2026-10-22 14:17:25 +02:00
jschoubben 138a1e1c4b issue 1838: refuse a node name of this mesh in the pacman and kernel modules
A module is for any mesh; the check reads the list of names at the root and the covered modules, and the kernel module's fixtures now say what they show.
2026-10-22 14:15:53 +02:00
jschoubben f1fbd91f0d Say a refusal the gate is already answering as stalled only once its fresh check did not cure it (ADR 0505 §4) 2026-10-22 14:15:49 +02:00
jschoubben 5ddc3ff312 Measure a repository's last check time from its newest verdict (ADR 0505 §4) 2026-10-22 14:13:18 +02:00
jschoubben b5908b486e Give the gate a settings file for the bounds ADR 0505 sets on saying a refusal and a needed new head 2026-10-22 14:12:31 +02:00
jschoubben aa57bbcd44 Tell a settled walk's end at the next flush, and prove the hold and the delivery line over the real bus (ADR 0505 §1-§3) 2026-10-22 14:11:16 +02:00
jschoubben 67a585600d End every problem message with whether a delivery was running on what it is about (ADR 0505 §1) 2026-10-22 14:10:06 +02:00
jschoubben 1caf728a4b Send at once what is raised on a failed walk's machines in the two minutes after it (ADR 0505 §6) 2026-10-22 14:07:51 +02:00
jschoubben 02816c0f7a Hold what a delivered walk touched until it has settled, and tell its end then (ADR 0505 §3, §7) 2026-10-22 14:07:15 +02:00
mesh-tools 7bf63b0ac5 Merge pull request 'The bus runs its upstream image with the mesh's programs mounted (issues 1640, 1641, 1642, ADR 0480 §1-§3, §5, §6; ADR 0497 §5; ADR 0506)' (#434) from feat/r4d into main 2026-10-22 12:05:13 +00:00
jschoubben 426db57d3f Hold what a walk's merges change on a machine it sends nothing to, and name only where it sent (ADR 0505 §2) 2026-10-22 14:04:06 +02:00
jschoubben 1d995d2bef Hold the controller's own lease, bus and health conditions while its delivery runs (ADR 0505 §2) 2026-10-22 14:03:09 +02:00
jschoubben 92225cd528 Make the settle after a delivered walk a setting, two minutes by default (ADR 0505 §3) 2026-10-22 14:02:17 +02:00
jochen 35e4764d35 Pin only-office's secure-link secret, so its document service and nginx sign and check Editor.bin links with one value across restarts (issue 1837)
mesh/merge-gate head 35e4764d354a merged into trunk 7bf63b0ac597 — pass: builds only-office → novox; no bus step; every machine composes with the change as…
mesh/check failed: unit: modules/gitea.test: [gitea] where the poll of merges last looked from could not be read, so none is kept — no runtime | 35e47…
mesh/repo-check head 35e4764d354a merged into trunk 7bf63b0ac597 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-22 13:40:42 +02:00
mesh-tools b7b9f2a3da Merge pull request 'Join nextcloud to only-office through a document-server provision, so OnlyOffice opens documents again (issue 1830)' (#438) from fix/1830-onlyoffice-link into main 2026-10-22 09:49:59 +00:00
jschoubben 68183ee441 Keep n8n on its own Dockerfile: since ADR 0487 a module may ship one, and the move to the upstream image only added a config value and a uuid mount (the operator, 2026-10-22)
mesh/delivery delivered
mesh/merge-gate head 68183ee4413d merged into trunk 7bf63b0ac597 — pass: group feat/r4d composed: every machine composes with the change as it did without …
mesh/delivery-group group feat/r4d delivered: every member is delivered
mesh/check failed: unit: clipmenu/cmd/clipmenu-tools.TestTheLauncherWaitsQuietlyWhileAnotherClipmenudHoldsTheLock: manifest_test.go:199: the launcher …
mesh/repo-check head 68183ee4413d merged into trunk b7b9f2a3da78 beside mesh-controller#433@707bfba4 — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.s…
2026-10-22 11:47:44 +02:00
jschoubben cff5ed46b7 Check an image's Dockerfile place instead of refusing the image kind (ADR 0487 clause 13)
mesh/check failed: unit: modules/gitea.test: [gitea] where the poll of merges last looked from could not be read, so none is kept — no runtime | cff5e…
mesh/merge-gate waits for its group's composed check
mesh/repo-check head cff5ed46b794 merged into trunk 529c156f6407 beside mesh-controller#433@707bfba4 — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.s…
mesh/delivery superseded: a newer head of the same pull request
mesh/delivery-group group feat/r4d checking: composed check out of date: novox/mesh-catalog@c948f3a61c74 was judged against trunk 848249cf54b6, the trunk is no…
2026-10-22 11:44:45 +02:00
jschoubben 7451e7edf4 Merge main into feat/r4d: keep the image check beside the image builder's two checks (ADR 0487 clause 13)
mesh/merge-gate waits for its group's composed check
mesh/repo-check head 7451e7edf4e5 merged into trunk 529c156f6407 beside mesh-controller#433@499ff5c2 — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.s…
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: unit: modules/gitea.test: ✔ the same merge, head or close announced twice has the same id, and another has another (6.449977ms) | 7…
2026-10-22 11:41:14 +02:00
mesh-tools 529c156f64 Merge pull request 'Hold a config value for the pull request that introduces its key (issue 1826, ADR 0502)' (#437) from fix/1826-held-config into main 2026-10-22 09:38:31 +00:00
jochen 47df3a4d60 Merge remote-tracking branch 'origin/main' into fix/1830-onlyoffice-link
mesh/check failed: unit: modules/gitea.test: [gitea] where the poll of merges last looked from could not be read, so none is kept — no runtime | 47df3…
mesh/merge-gate head 47df3a4d607a merged into trunk 529c156f6407 — pass: builds nextcloud, only-office → novox; no bus step; every machine composes with th…
mesh/delivery delivered
mesh/repo-check head 47df3a4d607a merged into trunk 529c156f6407 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
2026-10-22 11:30:58 +02:00
jochen 2d2e831659 Apply only-office's bound address and secret to onlyoffice settings every tick (issue 1830) 2026-10-22 11:30:26 +02:00
jochen d3ebaf72eb Require only-office's document server and carry its address and secret into nextcloud's config (issue 1830) 2026-10-22 11:27:49 +02:00
jochen 183b9680c1 Offer only-office's document server with its JWT as the shared credential (issue 1830) 2026-10-22 11:20:08 +02:00
mesh-tools 74baa53787 Merge pull request 'Give mesh-build-image the registry's address as the seat's reach (issue 1703)' (#436) from fix/1703-registry-binding into main 2026-10-22 09:14:12 +00:00
jschoubben 332aa75afd mesh-config: read the pull request's commit from head_sha (issue 1826)
mesh/delivery delivered
mesh/merge-gate head 332aa75afd19 merged into trunk 74baa5378716 — pass: group fix/1826-held-config composed: every machine composes with the change as it …
mesh/delivery-group group fix/1826-held-config delivered: every member is delivered
mesh/check failed: g14: novox/mesh-catalog#437: checks/imagebuild/go.mod cannot be read to check its mesh-sdk pin: Gitea API /repos/novox/mesh-catalog…
mesh/repo-check head 332aa75afd19 merged into trunk 74baa5378716 beside mesh-controller#434@4357b368 — pass: its merge-check.sh passed, each part in its to…
2026-10-22 11:09:48 +02:00
jschoubben 9e2d1164f0 Give mesh-build-image the registry's address as the seat's reach, which the provisioner's client fills (issue 1703)
mesh/merge-gate head 9e2d1164f06e merged into trunk 5a6d082e8c55 — pass: builds mesh-build-image, sent nowhere; no bus step; every machine composes with th…
mesh/repo-check head 9e2d1164f06e merged into trunk 5a6d082e8c55 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/check failed: unit: modules/gitea.test: [gitea] where the poll of merges last looked from could not be read, so none is kept — no runtime | 9e2d1…
2026-10-22 11:07:50 +02:00
jschoubben 48b08c74ab mesh-config: apply a held value only at the merged head; a close the forge cannot confirm drops nothing (issue 1826) 2026-10-22 11:07:19 +02:00
jschoubben 260be1c3e6 mesh-delivery-gate: ask a pull request's check again when a config value is held or dropped for it (issue 1826)
mesh-config raises config-changed (hold, drop) naming the pull request; the delivery, or its group's
composed check, is asked again on the gate's own path, with why in the transition. Not the recheck verb.
2026-10-22 11:01:07 +02:00
jschoubben 224a7a66a5 mesh-config: hold a value for the open pull request that introduces its key (issue 1826)
A key a pull request adds is declared by no registered manifest, so its value was refused and the
group's composed check could not pass. A proposal may now name the pull request: judged against what
the controller told of its head, asked of the operator, kept apart on Approve, applied at the merge,
dropped at a close.
2026-10-22 10:58:28 +02:00
mesh-tools 5a6d082e8c Merge pull request 'Add the image build module: buildah and mesh-build-image, rootless, publishing only from the trunk (issues 1702, 1703, 1704, 1707)' (#435) from feat/r4c9-image-build into main 2026-10-22 08:57:09 +00:00
jschoubben 7b5f7f84ac List mesh-build-image among the issue tracker's owners
mesh/repo-check head 7b5f7f84acb7 merged into trunk 848249cf54b6 — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/check failed: unit: modules/gitea.test: ✔ the same merge, head or close announced twice has the same id, and another has another (6.882981ms) | 7…
mesh/merge-gate head 7b5f7f84acb7 merged into trunk 848249cf54b6 — pass: group feat/r4c9-image-build composed: every machine composes with the change as it…
mesh/delivery delivered
mesh/delivery-group group feat/r4c9-image-build delivered: every member is delivered
2026-10-22 10:51:40 +02:00
jschoubben 3508be1476 Merge origin/main into feat/r4c9-image-build 2026-10-22 10:51:24 +02:00
jschoubben 7c5d11e55d Skip the real fetch test where buildah is not installed: every step is guarded by it
mesh/delivery-group group feat/r4c9-image-build checking: 0 of 2 member(s) ready
mesh/merge-gate group feat/r4c9-image-build: its group's composed check has not given a verdict at the members' heads yet
mesh/check failed: unit: i3/cmd/i3-tools.TestTheMainFileAndEveryModulesContributionLoadTogether: manifest_test.go:247: expected the launcher, clipboar…
mesh/delivery superseded: a newer head of the same pull request
2026-10-22 10:43:07 +02:00
jschoubben 3dcaa7a996 Fail a push whose digest file cannot be read; test the image steps through a real bus against captured shapes (issue 1703) 2026-10-22 10:34:37 +02:00
mesh-tools 848249cf54 Merge pull request 'Register the build and delivery modules as issue owners (issue 1815)' (#433) from fix/1815-tracker-owners into main 2026-10-22 08:30:41 +00:00
jschoubben 692fc3e1ab Add mesh-build-image: rootless image builds with buildah, pushed by tag from trunk only (issue 1703) 2026-10-22 10:27:01 +02:00
jschoubben ce8d52ed0b Let gitea read the image build module's keys, so its key is the builders' (issue 1703f) 2026-10-22 10:27:01 +02:00
jschoubben f5153a490c Check that every image builds from a Dockerfile in its own module's directory (issue 1707) 2026-10-22 10:27:01 +02:00
jschoubben 8815f950b0 Add the buildah module and the check that only it provides rootless-image-build (issues 1702, 1704) 2026-10-22 10:27:01 +02:00
jschoubben c948f3a61c Skip the bus's docker tests loudly where there is no docker, and run them by name in a docker step of the merge check where there is (ADR 0480 clause 6)
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: g14: novox/mesh-catalog#434 does not merge into the trunk: files in conflict: merge-check.sh | c948f3a61c74 on 529c156f6407 | unit …
mesh/delivery-group group feat/r4d rejected: a member's own check failed
mesh/merge-gate out of date: judged against trunk 5a6d082e8c55, the trunk is now 74baa5378716; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 5a6d082e8c55, the trunk is now 74baa5378716; judged again on a recheck or when it is merged
2026-10-22 10:18:15 +02:00
jschoubben 91877e3421 Put the bus's two mounted programs under a directory the module declares, as the controller's module check requires (review round 1, C1) 2026-10-22 10:04:31 +02:00
jschoubben 77b89ff182 Take the rehearsal tool out of the nats module: a new nats build is held until the bus upgrade itself, so it could only run after the step it rehearses (ADR 0236 section 36) 2026-10-22 10:01:23 +02:00
jschoubben e7ca330e1f Rehearse the bus upgrade on a copy of the last snapshot with a tool of the nats module, and run the docker tests only as integration tests (issue 1642, ADR 0480 clause 6) 2026-10-22 10:00:15 +02:00
jschoubben b4e90e5fce Run the bus from its pinned upstream image with the snapshot program and the entrypoint mounted, and write and rehearse the way there and back (issues 1640, 1641, 1642, ADR 0480 clauses 1-3, 5, 6, 11) 2026-10-22 09:55:03 +02:00
jschoubben e2220df5e8 Register the build and delivery modules as issue owners, and test that every code module is one (issue 1815)
mesh/check failed: unit: modules/gitea.test: ✔ the same merge, head or close announced twice has the same id, and another has another (9.586412ms) | e…
mesh/merge-gate head e2220df5e851 merged into trunk 97a111e70abf — pass: builds mesh-issues → novox; no bus step; every machine composes with the change as…
mesh/repo-check head e2220df5e851 merged into trunk 97a111e70abf — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-22 09:54:51 +02:00
jschoubben 0ec446ab75 n8n's media-gid is a required config value: a machine that does not say it refuses n8n (issue 1639) 2026-10-22 09:49:28 +02:00
jschoubben 3c0496d1d3 n8n runs its upstream image with uuid mounted and the media group joined, in place of a Dockerfile (issue 1639, ADR 0480 clauses 1, 3, 7, 8) 2026-10-22 09:49:28 +02:00
mesh-tools 97a111e70a Merge pull request 'Hold the Approve ask until a review names the head (issue 1803, ADR 0496)' (#432) from fix/1803-hold-ask-without-review into main 2026-10-21 23:48:59 +00:00
mesh-tools eb4f16431c Merge pull request 'Leave the go.mod fixtures under testdata out of the build module's tests and digests (issue 1810)' (#431) from fix/1810-beside-sibling into main 2026-10-21 23:44:20 +00:00
jschoubben c4fd31aa7e Say a held group waits for a review and keep each member's status current (issue 1803, round 1)
mesh/merge-gate head c4fd31aa7e97 merged into trunk eb4f16431ce3 — pass: builds mesh-delivery-gate → novox; no bus step; every machine composes with the ch…
mesh/repo-check head c4fd31aa7e97 merged into trunk eb4f16431ce3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/check failed: unit: clipmenu/cmd/clipmenu-tools.TestTheLauncherWaitsQuietlyWhileAnotherClipmenudHoldsTheLock: manifest_test.go:199: the launcher …
2026-10-22 01:37:49 +02:00
jschoubben 09d5360bce Follow the hold in the bus test: a head with no review is asked only once an edit names it (issue 1803)
mesh/merge-gate head 09d5360bce5a merged into trunk 4915f6985785 — pass: builds mesh-delivery-gate → novox; no bus step; every machine composes with the ch…
mesh/repo-check head 09d5360bce5a merged into trunk 4915f6985785 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/check failed: unit: clipmenu/cmd/clipmenu-tools.TestTheLauncherWaitsQuietlyWhileAnotherClipmenudHoldsTheLock: manifest_test.go:199: the launcher …
mesh/delivery superseded: a newer head of the same pull request
2026-10-22 01:27:18 +02:00
jschoubben b4d7a32abf Hold the Approve ask until a review names the head (issue 1803, ADR 0496)
Twice an unreviewed pull request was asked and merged on a warning line alone.
2026-10-22 01:25:15 +02:00
jschoubben 6aa4f135c9 Leave the go.mod fixtures under testdata out of the build module's tests and digests, so the checker stops running a fixture as a module (issue 1810)
mesh/check failed: unit: i3/cmd/i3-tools.TestTheMainFileAndEveryModulesContributionLoadTogether: manifest_test.go:247: expected the launcher, clipboar…
mesh/merge-gate head 6aa4f135c9f6 merged into trunk 4915f6985785 — pass: builds mesh-build-go → ace, g14, novox, shanks; no bus step; every machine compose…
mesh/repo-check head 6aa4f135c9f6 merged into trunk 4915f6985785 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-22 01:22:31 +02:00
mesh-tools 4915f69857 Merge pull request 'Add disk_folders to disk-load: which folders fill a disk (issue 1804)' (#430) from fix/1804-folder-sizes into main 2026-10-21 23:20:59 +00:00
jschoubben 843e408556 disk_folders: answer said as a list (issue 1804)
mesh/merge-gate head 843e40855684 merged into trunk 3bbae41037e3 — pass: builds disk-load → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head 843e40855684 merged into trunk 3bbae41037e3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/check failed: unit: clipmenu/cmd/clipmenu-tools.TestTheLauncherWaitsQuietlyWhileAnotherClipmenudHoldsTheLock: manifest_test.go:199: the launcher …
mesh/delivery delivered
2026-10-22 01:13:35 +02:00
jschoubben 4ef2fa2dff disk_folders: bound the walk at 25 s, below the tool runner's 30 s, and say findings as lines (issue 1804)
mesh/merge-gate waiting in the build queue since 01:13
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: unit: diskload/cmd/disk-load: # diskload/cmd/disk-load [diskload/cmd/disk-load.test] | 4ef2fa2dff1c on 3bbae41037e3 | unit 4096 run…
2026-10-22 01:13:29 +02:00
jschoubben 8288042eb7 Add disk_folders to disk-load: no tool said which folders fill a disk (issue 1804)
mesh/merge-gate building on shanks since 01:09
mesh/check failed: unit: clipmenu/cmd/clipmenu-tools.TestTheLauncherWaitsQuietlyWhileAnotherClipmenudHoldsTheLock: manifest_test.go:199: the launcher …
mesh/delivery superseded: a newer head of the same pull request
2026-10-22 01:02:31 +02:00
mesh-tools 3bbae41037 Merge pull request 'Start the gitea bundle's binds without awaiting them, so its handshake is answered (issue 1793)' (#429) from fix/1793-bind-after-handshake into main 2026-10-21 22:59:00 +00:00
jschoubben 77f3963d83 Test that the gitea bundle's import resolves while its binds are refused (issue 1793)
mesh/merge-gate head 77f3963d8338 merged into trunk c770f7947d95 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 77f3963d8338 merged into trunk c770f7947d95 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/check failed: unit: fixture/beside.TestReadsTheSiblingBesideIt: beside_test.go:17: open /var/lib/mesh-build-go-work/mvipvmt100c902/beside/sibling…
2026-10-22 00:49:27 +02:00
jschoubben 49a28f8537 Start the gitea bundle's binds without awaiting them, so its handshake is answered while a refused bind is asked again (issue 1793)
mesh/check failed: unit: modules/gitea.test: [gitea] where the poll of merges last looked from could not be read, so none is kept — no runtime | 49a28…
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head 49a28f85378b merged into trunk c770f7947d95 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 49a28f85378b merged into trunk c770f7947d95 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
2026-10-22 00:45:54 +02:00
mesh-tools c770f7947d Merge pull request 'Bind the gitea bundle's event consumers again until they hold, so verdicts reach the forge (issue 1793)' (#428) from fix/1793-verdict-reaches-the-forge into main 2026-10-21 22:44:16 +00:00
mesh-tools 4b8e7f11cf Merge pull request 'Make docker_volumes_remove_anonymous answer on a machine with over a thousand volumes (issue 1796)' (#427) from fix/1796-sizes-on-a-large-set into main 2026-10-21 22:38:36 +00:00
jschoubben 0d2686b5eb Bind the gitea bundle's event consumers again until they hold, so a refusal at its start no longer ends it and loses verdicts (issue 1793)
mesh/merge-gate head 0d2686b5ebee merged into trunk 4b8e7f11cf53 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 0d2686b5ebee merged into trunk 4b8e7f11cf53 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/check failed: unit: fixture/beside.TestReadsTheSiblingBesideIt: beside_test.go:17: open /var/lib/mesh-build-go-work/mvip9ircc11d3c/beside/sibling…
2026-10-22 00:37:30 +02:00
mesh-tools 839be0eb64 Merge pull request 'A delivery whose walk was folded into a batch follows that batch's walk and ends delivered (issue 1786)' (#426) from fix/1786-follow-taken-over-walk into main 2026-10-21 22:26:33 +00:00
jschoubben dc144a4e7d Count volumes a half-done batch already removed as removed, and test the batching (issue 1796)
mesh/merge-gate head dc144a4e7dd7 merged into trunk 839be0eb6444 — pass: builds docker → ace, g14, novox, shanks; no bus step; every machine composes with …
mesh/repo-check head dc144a4e7dd7 merged into trunk 839be0eb6444 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/check failed: unit: fixture/beside.TestReadsTheSiblingBesideIt: beside_test.go:17: open /var/lib/mesh-build-go-work/mvip2dsm29adef/beside/sibling…
2026-10-22 00:23:21 +02:00
jschoubben bfc42e4f4f Read volume sizes only when asked and remove in bounded batches, so docker_volumes_remove_anonymous answers on a machine with over a thousand volumes (issue 1796)
mesh/merge-gate building on shanks since 00:21
mesh/delivery superseded: a newer head of the same pull request
2026-10-22 00:20:32 +02:00
jschoubben c4176107f2 Keep a delivery superseded by its walk final, and heal it as delivered by the walk that took its walk over (issue 1786)
mesh/merge-gate head c4176107f287 merged into trunk a3794ea156dd — pass: builds mesh-delivery-gate → novox; no bus step; every machine composes with the ch…
mesh/repo-check head c4176107f287 merged into trunk a3794ea156dd — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivering: 0 machine step(s) passed
2026-10-22 00:01:22 +02:00
mesh-tools a3794ea156 Merge pull request 'Add a tool that removes leftover anonymous docker volumes (issue 1796)' (#425) from fix/1796-remove-anonymous-volumes into main 2026-10-21 21:54:26 +00:00
jschoubben e8cb2ee470 Wait for the batch that took a folded walk over, and let a delivery superseded by its walk follow that walk (issue 1786)
mesh/merge-gate building on ace since 23:57
mesh/delivery superseded: a newer head of the same pull request
2026-10-21 23:53:52 +02:00
jschoubben 07b3c5e77a Report each anonymous volume's size and the space freed (issue 1796)
mesh/merge-gate head 07b3c5e77a19 merged into trunk f3100d111633 — pass: builds docker → ace, g14, novox, shanks; no bus step; every machine composes with …
mesh/repo-check head 07b3c5e77a19 merged into trunk f3100d111633 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 23:46:46 +02:00
mesh-tools f3100d1116 Merge pull request 'The checker runs beside today's check: build modules merge and place siblings; with automx, provisioner and gate fixes' (#423) from feat/the-checker into main 2026-10-21 21:46:17 +00:00
jschoubben 64a96a13f7 Add docker_volumes_remove_anonymous: remove leftover unmounted anonymous volumes (issue 1796)
mesh/merge-gate building on ace since 23:45
mesh/delivery superseded: a newer head of the same pull request
2026-10-21 23:44:18 +02:00
mesh-tools f1fb73e12c Merge pull request 'Ask a group member's own repo-check when the group is held for it (issue 1792)' (#424) from fix/1792-recheck-stale-member into main 2026-10-21 21:38:09 +00:00
jschoubben d2faada61b Clear a member's own-ask mark when its own verdict arrives, so a later pending status is asked again (issue 1792)
mesh/merge-gate head d2faada61bf3 merged into trunk 379317589488 — pass: builds mesh-delivery-gate → novox; no bus step; every machine composes with the ch…
mesh/repo-check head d2faada61bf3 merged into trunk 379317589488 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 23:16:35 +02:00
jschoubben f436d1dace Ask a group member's own repo-check when the group is held before its Approve for it (issue 1792)
mesh/merge-gate waiting in the build queue since 23:13
mesh/delivery superseded: a newer head of the same pull request
2026-10-21 23:11:55 +02:00
jschoubben 12ff1cfbc4 Merge remote-tracking branch 'origin/fix/1106-sdk-member-merges' into feat/the-checker
mesh/repo-check head 12ff1cfbc434 merged into trunk f1fb73e12cee beside mesh-controller#430@b9ec1919 — pass: its merge-check.sh passed, each part in its to…
mesh/merge-gate head 12ff1cfbc434 merged into trunk f1fb73e12cee — pass: group feat/the-checker composed: every machine composes with the change as it did …
mesh/delivery delivered
mesh/delivery-group group feat/the-checker delivered: every member is delivered
2026-10-21 21:58:44 +02:00
jschoubben 7bf0de0ff5 Move the socket-return fill tests (issue 1165) to the integration level: they make a real socket and watch its directory 2026-10-21 21:57:53 +02:00
jschoubben 59b13d091d Ask no check of a group member that waits on its composed check, and stop asking a merge refused the same way with no trunk move (issue 1106) 2026-10-21 21:57:32 +02:00
jschoubben ce72a366bf Merge remote-tracking branch 'origin/feat/r4d-automx-go' into feat/the-checker 2026-10-21 21:50:26 +02:00
jschoubben d9876e5296 Merge remote-tracking branch 'origin/fix/1165-fill-retry' into feat/the-checker
# Conflicts:
#	modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client_test.go
2026-10-21 21:50:26 +02:00
jschoubben 62e67578eb Merge remote-tracking branch 'origin/fix/xterm-config-defaults' into feat/the-checker 2026-10-21 21:50:00 +02:00
jschoubben 1319432e55 Test that the integration level gets MESH_CHECK_BESIDE only when siblings were placed (issue 1677) 2026-10-21 21:49:37 +02:00
jschoubben 0cf7b2344f Write a moved contributor's declared defaults into the provisioner client tests' stand-in mesh-config (issue 1780) 2026-10-21 21:48:59 +02:00
jschoubben aa5d263c08 Test the returning socket by a real restart of the stand-in node-engine 2026-10-21 21:45:34 +02:00
jschoubben 769a8d6f6a Fill again what failed for want of the node-engine's write socket, when inotify says the socket is back (issue 1165) 2026-10-21 21:45:13 +02:00
jschoubben 34b310b9f8 Drop the unused imports from the Node fetch preview 2026-10-21 21:24:44 +02:00
jschoubben 33d8b71cf7 Drop the unused imports from the Node fetch preview 2026-10-21 21:24:33 +02:00
jschoubben aa0eaa6c52 Build the merge preview's module trees with mesh-sdk's ModuleTrees and re-pin mesh-sdk to 78364e31, so the two fetch steps share it (issues 1675, 1676) 2026-10-21 21:24:24 +02:00
jschoubben d5804927b3 Make the build modules' fetch merge the head onto a trunk commit and place siblings beside the tree, so the checker judges the merge and tests read their siblings (issues 1675, 1676, 1677) 2026-10-21 21:04:59 +02:00
mesh-tools 3793175894 Merge pull request 'Make minio's link-key secrets 40 characters and keep every mc argument's secret out of its errors (issue 1766)' (#422) from fix/minio-link-key-length into main 2026-10-21 19:00:13 +00:00
jschoubben b70f246622 Merge remote-tracking branch 'origin/main' into fix/minio-link-key-length
mesh/merge-gate head b70f24662298 merged into trunk cdbdb4747474 — pass: builds minio, sent nowhere; no bus step; 1 wait(s) for a person; every machine com…
mesh/repo-check head b70f24662298 merged into trunk cdbdb4747474 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 20:52:18 +02:00
mesh-tools cdbdb47474 Merge pull request 'xterm: every resource a config value, today's values as defaults (issue 1656)' (#416) from fix/1656-xterm-config into main 2026-10-21 18:36:28 +00:00
jschoubben 689e61081d Merge remote-tracking branch 'origin/main' into fix/minio-link-key-length
mesh/merge-gate head 689e61081df9 merged into trunk cdbdb4747474 — pass: builds minio, sent nowhere; no bus step; 1 wait(s) for a person; every machine com…
mesh/repo-check head 689e61081df9 merged into trunk cdbdb4747474 — fail: its merge-check.sh failed: mesh-controller: 1 problem(s) in 139 manifest(s): modul…
mesh/delivery superseded: a newer head of the same pull request
2026-10-21 20:22:24 +02:00
jschoubben c83f48b821 Make link-key secrets 40 characters and keep every mc argument's secret out of its errors (issue 1766) 2026-10-21 20:21:56 +02:00
mesh-tools db5e6c5b48 Merge pull request 'Name the wait in the Stop hook's suggested calls, route adopt's counter reset through fsys, and stop writing empty counter files' (#421) from fix/1749-1757-wait-list into main 2026-10-21 18:19:19 +00:00
jschoubben b9dcf19c3f Merge remote-tracking branch 'origin/main' into fix/1656-xterm-config
mesh/repo-check head b9dcf19c3f91 merged into trunk db5e6c5b48f4 beside mesh-controller#420@9d260412 — pass: its merge-check.sh passed, each part in its to…
mesh/merge-gate head b9dcf19c3f91 merged into trunk db5e6c5b48f4 — pass: group fix/1656-xterm-config composed: every machine composes with the change as it…
mesh/delivery failed: its walk failed, carrying its own commit: a gate on a first machine (what it carried put back), a build, a machine
mesh/delivery-group group fix/1656-xterm-config failed: a member failed
2026-10-21 19:43:29 +02:00
jochen e3a714d5f0 Stop writing an empty refusal-counter file on every stop of every session (issue 1749)
mesh/merge-gate head e3a714d5f0f0 merged into trunk c3564c1accf3 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head e3a714d5f0f0 merged into trunk c3564c1accf3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
writeRefusalCounters wrote "{}" and, since it now routes through fsys
(this branch's earlier commit), created the waits folder on every single
stop, even for a session that never had a wait: one counter file per
session id, piling up on a node that uses the wait list at all. It now
removes the file instead of writing empty counters, a no-op when there
was never one to begin with, so a stop with nothing to record leaves
neither the file nor the folder; a refusal still writes it, and the
stop right after the wait is done (or let through) removes it again.
2026-10-21 19:41:05 +02:00
jochen 08cec8cb1c Route adopt's refusal-counter reset through fsys, and name the wait in the Stop hook's suggestions (issues 1749, 1757)
issue 1749: AdoptName reset both session ids' refusal-counter files with
os.WriteFile, bypassing fsys; on a node whose agent has its own account
(ADR 0266) the module runs as the operator's account and cannot write
into the agent's home directly, so the reset silently failed. writeRefusalCounters
now goes through fsys (MkdirAll then WriteFile), as every other write into
the agent's home does; it is also called from the Stop hook itself, which
already runs as the agent's own account, where fsys is its zero value and
behaves exactly as the plain write did before.

issue 1757: the Stop hook's suggested watched and said calls left out `by`,
which claude_code_wait requires on every op, so a session following the
suggestion word for word was refused. Both now carry "by" from the wait's
own By field (already kept per wait since issue 1629). The SessionStart
hook's suggested adopt call already carried by; added a test for it too,
alongside the Stop hook's two, each extracting the exact JSON the hook
prints and feeding it back to the tool's own dispatch to prove it is
accepted as printed.
2026-10-21 19:35:33 +02:00
jschoubben 70500c21a8 Run mailu's automx as a Go program mounted into the mirrored alpine image, answering as captured but naming the IMAP server (issues 1631, 1632, 1700, 1701, ADR 0480)
Python automx2, its database and its automx_rows tool go; the container keeps its name, network and port, gains an http health.
2026-10-21 19:29:05 +02:00
mesh-tools c3564c1acc Merge pull request 'A session's wait list and the plugin's Stop hook, so a written-down wait is never left unwatched (ADR 0489, ADR 0490)' (#420) from feat/wait-list into main 2026-10-21 17:18:48 +00:00
jschoubben e4cbffbaa9 Merge remote-tracking branch 'origin/main' into fix/1656-xterm-config
mesh/merge-gate out of date: judged against trunk 7a7a4034852a, the trunk is now c3564c1accf3; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 7a7a4034852a, the trunk is now c3564c1accf3; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
2026-10-21 19:16:12 +02:00
jochen 0f159a1662 Fix review round 2 of the wait list and its hooks (issues 1629, 1743, 1744, ADR 0490)
mesh/merge-gate head 0f159a166200 merged into trunk 7a7a4034852a — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head 0f159a166200 merged into trunk 7a7a4034852a — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The Stop hook treated a missing waits file exactly like one that could
not be read: a notice on every stop of every session with no waits —
the normal state, not an exceptional one. DecideStop now treats
os.ErrNotExist as the ordinary case of no open waits, silently; the
notice is still said for a file that exists but cannot be read or does
not parse.

ADR 0490 §1 keeps waits under a name, but the file a hook reads is kept
per session id: writing it from only the name an op just acted on meant
a second name bound to the same session id had its waits silently drop
out of enforcement the moment the first name was touched, and adopting
one name away emptied the id's file outright even when another name was
still bound to it. Every write now rewrites a session's file from the
union of every name currently bound to it (sessionWaitsUnion), adopt's
rewrite of the superseded id included.

Also: each atomic write gets its own temp file name, so two writes to
the same path can never interleave on one; adopt clears the refusal
counter of both the superseded and the adopting session, so a fresh
three-strikes count starts under the new binding; and list is read-only
again — it no longer claims an unbound name or refuses a session that
is not (yet) the one bound to it, and instead says who currently holds
the name.
2026-10-21 18:43:03 +02:00
jochen f0beea153e Key a wait list by name, not session id, so it survives a /clear (issues 1629, 1744, ADR 0490)
Claude Code gives a session a new session_id at /clear, with nothing
naming the earlier one, so keying claude_code_wait's state by session id
lost a session's waits the moment it cleared. ADR 0490 changes ADR 0489
§1: waits are now kept under the caller's name and node (by, who@node);
every op also takes session, the caller's current id, and the module
keeps a binding of the name to the session that last called for it. An
unbound name is claimed by the first call for it; a session whose own
id no longer matches the binding is refused on every op, naming the
session that holds it, until it calls the new op adopt, which always
takes the name over at once (no bound protects a recent call) and tells
the earlier session so on its own next call.

The mirrored file a hook reads stays keyed by session id (a hook only
ever has that), holding whichever name's waits are currently bound to
it; adopting rewrites both the old id's file (emptied) and the new id's
(filled) in the same call. A second mirrored file, the node-wide index,
lists every name on the node with an open wait; the SessionStart hook
now also reads it, on every start, and prints the exact adopt call for
each name, so a session with a brand new id can find and reclaim its
own waits without calling back into the module.
2026-10-21 18:32:50 +02:00
mesh-tools 7a7a403485 Merge pull request 'Files go through the store: object-store links, node-files upload/download, a document converter, and brother printing a key, a path on any machine or a web link (design 60 F3, ADR 0468)' (#417) from feat/files-through-the-store into main 2026-10-21 16:31:22 +00:00
jochen 02a8837348 Fix review round 1 of the wait list and its hooks (issues 1629, 1743, 1744)
A session id reaching the Stop and SessionStart hooks was only checked
for blank, so "../../x" or "a/b" could read or write outside the waits
directory; both hooks now hold every session id to waitSessionID before
building any path, and allow (saying why) rather than fail on a bad one.
claude_code_wait's list/watched/done/said held only add to that same
check, letting a malformed id through as an ordinary, always-empty key;
they all refuse it now.

The Stop hook's three-strikes cap could be bypassed forever if its
counter file could not be written on a refusal: it now allows instead,
saying so, so a wait can never hold a turn open indefinitely. Its advice
offered mesh_wait on a wait's event family unconditionally, ignoring
that the session's tool runner may not be granted it (ADR 0367 §2); it
now offers mesh_wait only "if this session's tool runner is granted it,
otherwise start a watcher", and drops the family entirely when a wait
has none.

Both hooks' fail-open and let-through notices were said only on stderr,
which Claude Code does not show the operator on an allowed (exit 0)
outcome; they now also print the same words as a "systemMessage" JSON
object on stdout, which Claude Code does show.
2026-10-21 18:23:42 +02:00
jochen 0fa427dad3 Add the plugin's Stop hook so a written-down wait is never left unwatched (issue 1743)
A session could end its turn while a wait it wrote down with
claude_code_wait had nothing following it. The plugin's Stop hook
(claude-code stop <waits-dir>) refuses the stop while a wait is within
its bound and unwatched, or past its bound and not said, reading the
waits file directly with no network and a 5s bound; it fails open on a
missing or malformed file or a panic, and lets a wait through after
three refusals in a row rather than holding the session forever.
Renames TestThePluginsOnlyHookIsTheGuard to cover the session-start and
Stop hooks too.
2026-10-21 18:08:13 +02:00
jochen 37628f4bce Add the plugin's SessionStart hook so a session learns its own id (issue 1744)
A session is never told its session id by any tool, only by a hook's
stdin, so it cannot name itself to claude_code_wait. The plugin's
SessionStart hook (claude-code session-start <waits-dir>) prints the id
and how to give it, plus the session's still-open waits after a /clear,
reading the waits file claude_code_wait mirrors directly rather than
calling back into this module. Carries the Stop hook's command and the
GuardFiles.WaitsDir plumbing MarketplaceWith needs for both (issue 1743
wires the Stop hook's dispatch next).
2026-10-21 18:07:36 +02:00
jschoubben 6cf51173d5 Give the Service its own fetch client and open-document waits instead of reading package variables from background goroutines, which a test or reload changed meanwhile: data races under -race (issue 1601)
mesh/repo-check head 6cf51173d586 merged into trunk b2f5ebb27dbf beside mesh-controller#424@550cf37d — pass: its merge-check.sh passed, each part in its to…
mesh/merge-gate head 6cf51173d586 merged into trunk b2f5ebb27dbf — pass: group feat/files-through-the-store composed: every machine composes with the chang…
mesh/delivery superseded: its walk was superseded and names no walk that took it over: a walk that took it over answers its merge, and it follows that wa…
mesh/delivery-group group feat/files-through-the-store stopped: superseded, not carried by a later delivery: novox/mesh-catalog@6cf51173d586, novox/mesh-contro…
2026-10-21 18:03:58 +02:00
jschoubben ad85a979e3 Read the upload fake under a lock, since the print runs in the background and the test read it meanwhile: a data race under -race (issue 1598) 2026-10-21 18:03:58 +02:00
jochen 56c68be7fa Keep a session's wait list in the claude-code module (issue 1629)
A session can end its turn while an Approve it expects is still open and
nothing then watches it. claude_code_wait (add/watched/done/said/list)
keeps each session's waits in the module's waits state, keyed by its
session id and node, and mirrors them whole and atomically to a file
under the agent's home that the plugin's coming hooks (1743, 1744) will
read without calling back into this module.
2026-10-21 17:59:10 +02:00
jschoubben aef2a1a91f Merge remote-tracking branch 'origin/main' into feat/files-through-the-store
mesh/delivery-group group feat/files-through-the-store rejected: a member's own check failed
mesh/merge-gate waits for its group's composed check
mesh/repo-check head aef2a1a91ffc merged into trunk b2f5ebb27dbf beside mesh-controller#424@550cf37d — fail: its merge-check.sh failed: --- FAIL: TestAScan…
mesh/delivery superseded: a newer head of the same pull request
2026-10-21 17:49:29 +02:00
mesh-tools b2f5ebb27d Merge pull request 'Fill a delivery made from its verdict from its pull request's announcement, and say what it waits for (issue 1726)' (#419) from fix/1726-delivery-from-verdict into main 2026-10-21 15:40:16 +00:00
mesh-tools 3259765d46 Merge pull request 'Read automx's rows in a mode its image's sqlite3 knows (issue 1630)' (#418) from fix/1630-automx-rows-sqlite into main 2026-10-21 15:08:27 +00:00
jschoubben 4b9464f3ce Supersede older heads when an announcement or edit fills a delivery made from its verdict, and say truthfully what fills it (issue 1726)
mesh/merge-gate head 4b9464f3ceeb merged into trunk 3259765d46bc — pass: builds mesh-delivery-gate → novox; no bus step; every machine composes with the ch…
mesh/repo-check head 4b9464f3ceeb merged into trunk 3259765d46bc — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery covered: a later merge that contains it was delivered: novox/mesh-catalog@6cf51173d586 (merged as 7a7a4034 into main, walk plan-17925972200…
2026-10-21 17:06:51 +02:00
jschoubben 1f7dd702d2 Read automx's rows with a mode its image's sqlite3 knows, since it refuses -json (issue 1630)
mesh/merge-gate head 1f7dd702d283 merged into trunk 90ecba4e50a3 — pass: builds mailu, sent nowhere; no bus step; 1 wait(s) for a person; every machine com…
mesh/repo-check head 1f7dd702d283 merged into trunk 90ecba4e50a3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 16:24:58 +02:00
jschoubben dd31d991cd Fill a delivery made from its verdict from its pull request's announcement, and say what it waits for (issue 1726) 2026-10-21 16:18:43 +02:00
jschoubben a2deae03b7 Answer printer.print within 25 s as started with its ask id, and read it with printer.print {ask}, so no call is cut by the tool runner (issue 1598)
mesh/merge-gate checking again: rejected at 16:52 only because the group sibling mesh-controller#424 (227c86eb) no longer merged into main; the sibling's n…
mesh/repo-check checking again: rejected at 16:52 only because the group sibling mesh-controller#424 (227c86eb) no longer merged into main; the sibling's n…
mesh/delivery-group group feat/files-through-the-store checking: 0 of 2 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
2026-10-21 15:25:17 +02:00
jschoubben 72ef20a4e7 Follow a conversion with convert {ask}, each call under 25 s, and leave the SDK's call timeout alone (issue 1600) 2026-10-21 15:23:33 +02:00
jschoubben 778356f10c Answer a conversion not done in 25 s as converting and read it with convert {ask}, so no call is cut by the tool runner (issue 1595) 2026-10-21 15:22:36 +02:00
jschoubben 7c97ce0bf8 Bound an upload and a download to 25 s, so a tool call is never cut while the transfer goes on (issue 1597) 2026-10-21 15:21:56 +02:00
jschoubben 68e1ad80ac Declare the seat document-converter in only-office's seats (issue 1595) 2026-10-21 15:19:03 +02:00
jschoubben 6e2762d943 Declare the seat object-store in minio's seats (issue 1516) 2026-10-21 15:19:03 +02:00
jschoubben dc2b0641bf Merge remote-tracking branch 'origin/main' into feat/files-through-the-store 2026-10-21 15:10:43 +02:00
jschoubben b54640e386 Post conversions to the document server's documented /converter path (issue 1596) 2026-10-21 15:10:35 +02:00
jschoubben 6358041eca Convert a Word, spreadsheet, slide, text or web document to PDF through the converter before printing it (issue 1600) 2026-10-21 15:09:47 +02:00
jschoubben 657b38ccd1 Print a web link: the device process fetches it bounded, never inside the mesh or the machine, stores it under its SHA-256 and prints that (issue 1599) 2026-10-21 15:07:34 +02:00
jschoubben e959cf339b Make printer.print take exactly one of object, path with node, or url, send a machine's file through its node-files, and check the printer's formats first (issue 1598) 2026-10-21 15:05:03 +02:00
jschoubben cb226c6fef File a scan from the store by the link the device process signs, not by parts over the bus (issue 1601) 2026-10-21 15:01:09 +02:00
jschoubben 278014fd79 Add the converter process: take convert-asks, have the document server make the PDF with a token only it holds, and put it to the target link (issue 1596) 2026-10-21 14:58:41 +02:00
jschoubben caae838a2a Claim document-converter and serve convert: queue the ask for the converter and read its answer, holding no credential (issue 1595) 2026-10-21 14:55:47 +02:00
jschoubben 6ee3507af0 Serve node-files.upload and download: a machine's file goes into a bucket and an object comes out of one, by short links, never over the bus (issue 1597) 2026-10-21 14:54:17 +02:00
jschoubben 06ca1b7fb9 Serve object-store.link: a signed link to one object, signed with the bucket's link key, for the consumer and its shares; retire the root-signed minio_presigned_url (issue 1516) 2026-10-21 14:52:14 +02:00
mesh-tools 90ecba4e50 Merge pull request 'Capture automx's live answers as fixtures and add a read-only tool for its database rows, before R4d replaces it (issue 1630)' (#415) from feat/r4d-automx into main 2026-10-21 12:47:49 +00:00
jschoubben 5b3fd1008a Make a link key per bucket, limited to get and put of its objects and kept as minio's own secret, so links are not signed by the root account (issue 1594) 2026-10-21 14:47:14 +02:00
mesh-tools 671ab9a3ba Merge pull request 'Pin gitea, keycloak, mesh-provisioner-client, postgres and route-adapter to a mesh-sdk commit on main, so a fresh go mod download resolves them once the feature branch goes (issue 1663)' (#414) from fix/1663-sdk-pins-on-main into main 2026-10-21 12:30:06 +00:00
jschoubben 0a7c58766c Pin gitea, keycloak, mesh-provisioner-client, postgres and route-adapter to a commit of mesh-sdk main, since the pinned one is on a feature branch only (issue 1663)
mesh/merge-gate head 0a7c58766c20 merged into trunk 4efac366cebb — pass: builds gitea, keycloak, mesh-provisioner-client, postgres, route-adapter → ace, g1…
mesh/repo-check head 0a7c58766c20 merged into trunk 4efac366cebb — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivering: 0 machine step(s) passed
2026-10-21 14:07:01 +02:00
jschoubben 454c5662dd Add automx_rows, a read-only tool for the rows automx answers from (issue 1630)
mesh/merge-gate head 454c5662ddcb merged into trunk 671ab9a3ba7b — pass: builds mailu, sent nowhere; no bus step; 1 wait(s) for a person; every machine com…
mesh/repo-check head 454c5662ddcb merged into trunk 671ab9a3ba7b — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivering: 0 machine step(s) passed
2026-10-21 14:02:37 +02:00
jschoubben 8fba5e831b Capture what automx answers today, before a Go program replaces it (issue 1630) 2026-10-21 13:59:58 +02:00
mesh-tools 4efac366ce Merge pull request 'Pin mesh-provisioner, mesh-vault and rofi to a mesh-sdk commit on main, so a fresh go mod download (mesh-build-go's fetch) resolves them (issue 1659)' (#413) from fix/1659-fetch-pseudo-version into main 2026-10-21 11:40:06 +00:00
mesh-tools 38ac738d84 Merge pull request 'Add netcheck_request: one HTTP request and its whole answer, so a service's live answers can be captured, refusing loopback, unspecified and link-local addresses (issue 1643)' (#412) from fix/1643-http-request into main 2026-10-21 11:27:33 +00:00
jschoubben 92c5e1d9ae Pin mesh-provisioner, mesh-vault and rofi to a commit of mesh-sdk main, since the pinned one is on no branch and no fetch can resolve it (issue 1659)
mesh/merge-gate head 92c5e1d9aea8 merged into trunk 38ac738d8463 — pass: builds mesh-provisioner, mesh-vault, rofi → g14, novox, shanks; no bus step; every…
mesh/repo-check head 92c5e1d9aea8 merged into trunk 38ac738d8463 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 13:14:57 +02:00
jschoubben a01e1ea7fb Refuse a host in a short or legacy numeric form (127.1, 2130706433, 0x7f.0.0.1) in netcheck_request before any lookup, so the refusal does not depend on the resolver
mesh/merge-gate head a01e1ea7fb64 merged into trunk d74c1cb995a4 — pass: builds netcheck → ace, g14, novox, shanks; no bus step; every machine composes wit…
mesh/repo-check head a01e1ea7fb64 merged into trunk d74c1cb995a4 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 13:07:08 +02:00
mesh-tools d74c1cb995 Merge pull request 'Read manifests that name shares: mesh-build-go and mesh-build-node take mesh-sdk 465beb4 (issue 1657)' (#411) from feat/files-through-the-store-builders into main 2026-10-21 11:01:03 +00:00
jschoubben 2d30dc2f1e Read manifests that name shares: mesh-build-go and mesh-build-node take mesh-sdk 465beb4 (issue 1657)
mesh/merge-gate head 2d30dc2f1ef7 merged into trunk b5dbfbb78450 — pass: builds mesh-build-go, mesh-build-node → g14, novox, shanks; no bus step; every mac…
mesh/repo-check head 2d30dc2f1ef7 merged into trunk b5dbfbb78450 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 12:52:28 +02:00
jschoubben f178aab641 Make every xterm resource a config value with today's value as default; tools read the rendered resources (issue 1656)
mesh/delivery-group group fix/1656-xterm-config rejected: a member's own check failed
mesh/merge-gate waits for its group's composed check
mesh/repo-check head f178aab641dc merged into trunk 3259765d46bc beside mesh-controller#420@69a67876 — fail: its merge-check.sh failed: files asking for a …
mesh/delivery superseded: a newer head of the same pull request
2026-10-21 12:51:40 +02:00
mesh-tools b5dbfbb784 Merge pull request 'Pack archive artifacts in mesh-build-go, a module's directory or a pinned file, and refuse any path that leads out of the module through a link (issue 1589, ADR 0478 §1)' (#408) from fix/1589-archive-artifacts into main 2026-10-21 10:34:00 +00:00
mesh-tools e9e85365b5 Refuse loopback, unspecified and link-local addresses in netcheck_request, on the address dialled, and use no proxy 2026-10-21 10:27:44 +00:00
mesh-tools 710cfbd622 Merge pull request 'Let the build modules answer their own step results and toolchains, so a fetch's workspace and a build's digest can be read (issue 1562, build-module half)' (#410) from fix/1562-read-steps into main 2026-10-21 10:26:13 +00:00
mesh-tools 940594b074 Merge pull request 'Make dev-tools, lab and the build modules need the go and nodejs modules instead of declaring the toolchain packages, step B of issue 1573 (ADR 0477 §4)' (#409) from fix/1573b-toolchain-users into main 2026-10-21 10:21:02 +00:00
mesh-tools ff849d9f62 Add netcheck_request: one HTTP request and its whole answer, so a service's live answers can be captured (issue 1643) 2026-10-21 10:08:59 +00:00
mesh-tools f0c8958dd2 Merge pull request 'Apply a patch in the forge tool itself when the forge's diffpatch answers 500, so a patch whose context holds a non-ASCII character is taken (issue 1604)' (#407) from fix/1604-patch-applied into main 2026-10-21 10:08:48 +00:00
mesh-tools c33b83dce7 Read a build module's steps and toolchains states with its own tools, so a step's result and the reported toolchain can be checked live (issue 1562)
mesh/merge-gate head c33b83dce7d5 merged into trunk 940594b07478 — pass: builds mesh-build-go, mesh-build-node → g14, novox, shanks; no bus step; every mac…
mesh/repo-check head c33b83dce7d5 merged into trunk 940594b07478 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery covered: a later merge that contains it was delivered: novox/mesh-catalog@e023f69989ab (merged as b5dbfbb7 into main, walk plan-17925769516…
2026-10-21 09:57:16 +00:00
mesh-tools 36259f3406 Update the build modules' manifest tests for the toolchain provisions (issue 1573)
mesh/merge-gate head 36259f3406f7 merged into trunk f0c8958dd217 — pass: builds dev-tools, lab, mesh-build-go, mesh-build-node → ace, g14, novox, shanks; n…
mesh/repo-check head 36259f3406f7 merged into trunk f0c8958dd217 — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery covered: a later merge that contains it was delivered: novox/mesh-catalog@e023f69989ab (merged as b5dbfbb7 into main, walk plan-17925769516…
2026-10-21 09:53:48 +00:00
mesh-tools e023f69989 Compare an archive's tar stream with the build agent's always and its digest only under the Go it was captured with (issue 1589)
mesh/merge-gate head e023f69989ab merged into trunk 710cfbd622d7 — pass: builds mesh-build-go → g14, novox, shanks; no bus step; every machine composes wit…
mesh/repo-check head e023f69989ab merged into trunk 710cfbd622d7 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 09:52:22 +00:00
mesh-tools 324e331184 Anchor patch hunks as git apply does, refuse patches touching non-100644 files by their real mode, survive bare-CR context lines (issue 1604 review)
mesh/merge-gate head 324e331184da merged into trunk 0f2cee1bea4e — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 324e331184da merged into trunk 0f2cee1bea4e — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery covered: a later merge that contains it was delivered: novox/mesh-catalog@e023f69989ab (merged as b5dbfbb7 into main, walk plan-17925769516…
2026-10-21 09:52:10 +00:00
mesh-tools d8d153295a Say in the go and nodejs READMEs that they are now the only declarers (issue 1573) 2026-10-21 09:51:22 +00:00
mesh-tools bda75011a1 Say in the build modules' READMEs where the packages come from (issue 1573) 2026-10-21 09:50:55 +00:00
mesh-tools 9d1f152603 Wire the toolchains check into the merge check (issue 1573) 2026-10-21 09:50:46 +00:00
mesh-tools c4cd7a1b16 Check that only the go and nodejs modules declare the toolchain packages (issue 1573) 2026-10-21 09:50:14 +00:00
mesh-tools 7753c27042 Make dev-tools, lab and the two build modules require the toolchain provisions instead of declaring the packages (issue 1573) 2026-10-21 09:49:50 +00:00
mesh-tools ef154705b9 Make an archive's digest independent of the umask of the account that packs it (issue 1589) 2026-10-21 09:49:13 +00:00
mesh-tools d7e6074d97 Refuse a build output directory that is a link out of the module (issue 1589) 2026-10-21 09:46:47 +00:00
mesh-tools 800d6624d5 Refuse an archive of a module directory that is a link out of the workspace's tree (issue 1589) 2026-10-21 09:45:58 +00:00
mesh-tools 0f2cee1bea Merge pull request 'Add the go and nodejs modules that own the toolchain packages on a node, step A of issue 1573 (ADR 0477 §4)' (#406) from fix/1573a-toolchain-owners into main 2026-10-21 09:36:49 +00:00
mesh-tools 5efd84e93e Merge pull request 'Build with the node's installed Go and Node, never a pinned or downloaded toolchain, so builds match what the nodes run (issue 1572, ADR 0477)' (#403) from fix/1572-installed-toolchain into main 2026-10-21 09:27:17 +00:00
mesh-tools ceb42b0e92 Say in the go and nodejs READMEs what holds at this step, and what holds once the users require them (review of issue 1573 step A)
mesh/merge-gate head ceb42b0e920e merged into trunk 5efd84e93e38 — pass: builds new: modules/go, modules/nodejs, sent nowhere; no bus step; every machine c…
mesh/repo-check head ceb42b0e920e merged into trunk 5efd84e93e38 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 09:25:23 +00:00
mesh-tools e5cfcb316a Fall back to the tool's own patch applier when the forge answers 500, with tests (issue 1604) 2026-10-21 09:21:59 +00:00
mesh-tools 025d62478a Add the go and nodejs modules, which own the toolchain packages and provide go-toolchain and node-toolchain (issue 1573) 2026-10-21 09:21:19 +00:00
mesh-tools e6053262e4 Apply a unified diff in the forge tool itself, for the patches the forge's diffpatch refuses (issue 1604) 2026-10-21 09:20:25 +00:00
mesh-tools 6993ef003c Refuse an archive whose directory leads out of the module through a symlinked parent (review of issue 1589) 2026-10-21 09:19:03 +00:00
mesh-tools 406ee69732 Add the pinned release the archive tests serve (issue 1589) 2026-10-21 09:13:40 +00:00
mesh-tools 8bfa626a8b Test archive artifacts against build-agent's own digests (issue 1589) 2026-10-21 09:13:32 +00:00
mesh-tools bfd6b7c7ce Say the build tool packs archives, give the module a client for pinned files, and record how the digests were captured (issue 1589) 2026-10-21 09:12:55 +00:00
mesh-tools 640c6f9b0c Hand an archive artifact to the packing step (issue 1589, ADR 0478 §1) 2026-10-21 09:12:23 +00:00
mesh-tools d369adf245 Unpack and pack archive artifacts in mesh-build-go: a module's directory or a pinned file (issue 1589, ADR 0478 §1) 2026-10-21 09:11:29 +00:00
mesh-tools d8f87808b5 Add the archive fixture tree for mesh-build-go's archive tests (issue 1589) 2026-10-21 09:10:41 +00:00
mesh-tools 17d2f7fd5d Merge pull request 'End a stranded stop whose modules no merge can carry, so its alert clears instead of asking the operator for ever (issue 1581)' (#405) from fix/1581-stranded-removed into main 2026-10-21 08:40:20 +00:00
mesh-tools 6411eb4614 End a stranded stop whose modules no merge can carry, so stalled stops listing it (issue 1581)
mesh/merge-gate head 6411eb4614a7 merged into trunk 50ef13d29385 — pass: builds mesh-delivery-gate → novox; no bus step; every machine composes with the ch…
mesh/repo-check head 6411eb4614a7 merged into trunk 50ef13d29385 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
A stop for a walk that built none of its modules waited for a later merge to
carry them for ever when the pull request removed the module or its plan sent
it nowhere. It now reads ended, as covered reads covered, and leaves stalled.
2026-10-21 08:28:20 +00:00
mesh-tools 50ef13d293 Merge pull request 'Upgrade every node's packages on a Saturday 04:00 timer and make its outcome the pacman module's health, so the nodes stay on the same versions (issue 1574, ADR 0477)' (#404) from fix/1574-weekly-upgrade into main 2026-10-21 08:15:53 +00:00
mesh-tools bd10ec4f0d Say a weekly upgrade that hangs past two hours, so a stuck pacman lock is loud (issue 1574, ADR 0477)
mesh/merge-gate head bd10ec4f0dbe merged into trunk 4bdacf38d2f0 — pass: builds pacman → ace, g14, novox, shanks; no bus step; every machine composes with …
mesh/repo-check head bd10ec4f0dbe merged into trunk 4bdacf38d2f0 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 08:08:04 +00:00
mesh-tools 4bdacf38d2 Merge pull request 'Propose mesh-issues' owners: mesh-config takes the SDK that reads names with digits as names (issue 1439)' (#402) from feat/1439-name-runs into main 2026-10-21 08:07:38 +00:00
mesh-tools d699708fb3 Say in mesh-build-go's README that it builds with the installed go and its digest holds no Go version (issue 1572, ADR 0477)
mesh/merge-gate head d699708fb382 merged into trunk 17d2f7fd5df1 — pass: builds mesh-build-go, mesh-build-node → g14, novox, shanks; no bus step; every mac…
mesh/repo-check head d699708fb382 merged into trunk 17d2f7fd5df1 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 08:07:15 +00:00
mesh-tools e8046fca87 Say in the build modules' READMEs that they build with the installed toolchain, the digest holds no Go version, and the reference is agreement between build nodes (issue 1572, ADR 0477) 2026-10-21 08:05:55 +00:00
mesh-tools edec7063b3 Build TypeScript with the installed node and name no Node version (issue 1572, ADR 0477) 2026-10-21 08:04:41 +00:00
mesh-tools fc4089cde9 Test the Go module's steps and guard against the installed go (issue 1572, ADR 0477) 2026-10-21 08:03:45 +00:00
mesh-tools 049202d373 Build Go with the installed go and name no Go version (issue 1572, ADR 0477) 2026-10-21 08:02:54 +00:00
mesh-tools 7fcb9a743b Replace the downloaded release's tests with tests of the installed toolchain (issue 1572, ADR 0477) 2026-10-21 08:02:02 +00:00
mesh-tools d4d1a1591b Name pacman_upgrade_health in the pacman README's tools (issue 1574) 2026-10-21 08:00:39 +00:00
mesh-tools 5123c1dd57 Say the weekly upgrade and its health in the pacman README (issue 1574, ADR 0477) 2026-10-21 08:00:30 +00:00
mesh-tools e3ac017c28 Test the weekly upgrade's timer and its health (issue 1574, ADR 0477) 2026-10-21 08:00:21 +00:00
mesh-tools 1f5046f47e Declare the weekly upgrade timer with its health, and serve pacman_upgrade_health (issue 1574, ADR 0477) 2026-10-21 07:59:55 +00:00
mesh-tools f12f87f1f9 The weekly upgrade's timer, service and health tool (issue 1574, ADR 0477) 2026-10-21 07:59:31 +00:00
mesh-tools e319f9877f Upgrade every node's packages together each Saturday at 04:00, with the outcome as the pacman module's health, so the nodes keep the same toolchain versions (issue 1574, ADR 0477) 2026-10-21 07:58:01 +00:00
jochen 2379027cec Propose mesh-issues' owners: take the SDK that reads names with digits as names (issue 1439)
mesh/repo-check head 2379027ceca5 merged into trunk f813be275306 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/merge-gate head 2379027ceca5 merged into trunk f813be275306 — pass: group feat/1439-name-runs composed: every machine composes with the change as it d…
mesh/delivery delivered
mesh/delivery-group group feat/1439-name-runs delivered: every member is delivered
mesh-config refused every owners proposal as carrying a secret, because the SDK's CheckSecret took
modules/asus-zephyrus-g14, i3status-rust and powerlevel10k for random strings. The SDK at
feat/1439-name-runs judges such a lowercase name piece by piece; this moves mesh-config to it and
proves the catalogue's own owners list plus okioki is put to the operator.
2026-10-21 09:57:56 +02:00
mesh-tools cf197fee89 Upgrade every node's packages together each Saturday at 04:00, with the outcome as the pacman module's health, so the nodes keep the same toolchain versions (issue 1574, ADR 0477) 2026-10-21 07:57:48 +00:00
mesh-tools f813be2753 Merge pull request 'Declare scanner.scan's apart argument, so several tickets on the glass can be filed apart (issue 1528)' (#401) from fix/1528-apart-argument into main 2026-10-21 07:33:45 +00:00
jschoubben c4913dbbcb Declare scanner.scan's apart argument, so the console lets it through to the tool (issue 1528)
mesh/merge-gate head c4913dbbcb35 merged into trunk 9daeab209ba4 — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head c4913dbbcb35 merged into trunk 9daeab209ba4 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 09:26:11 +02:00
mesh-tools 9daeab209b Merge pull request 'Build Go modules with Go's own release of the recipe's version, so the distribution's build no longer makes the guard refuse (issue 1560, ADR 0476)' (#400) from fix/1560-go-release-toolchain into main 2026-10-21 06:47:11 +00:00
mesh-tools 3c4deb8bee Name the cause of a failed toolchain or module download, not Go's progress line before it (issue 1560, review round 1)
mesh/merge-gate head 3c4deb8bee8b merged into trunk 76bb22c36270 — pass: builds mesh-build-go → g14, novox, shanks; no bus step; every machine composes wit…
mesh/repo-check head 3c4deb8bee8b merged into trunk 76bb22c36270 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 00:59:12 +00:00
mesh-tools 4605fcf46d Fill the shared module cache with the release by the probe before the real-go integration tests, and skip loudly when it cannot (issue 1560, ADR 0476) 2026-10-21 00:56:03 +00:00
mesh-tools c5f8f81a07 Test that every step and the probe run Go's own release and the probe fills the module cache (issue 1560, ADR 0476) 2026-10-21 00:51:40 +00:00
mesh-tools 3f60d1ff31 Build Go modules with Go's own release of the recipe's version, put in the module cache by the probe, so the distribution's build no longer makes the guard refuse (issue 1560, ADR 0476) 2026-10-21 00:50:56 +00:00
mesh-tools 76bb22c362 Merge pull request 'Polish the brother module so the scanner and printer just work: long feeder scans, several tickets, joined glass pages, one-call print, certificate health' (#399) from feat/brother-polish into main 2026-10-21 00:22:44 +00:00
mesh-tools 9aa9792614 Merge pull request 'Fill a module's files one by one, so a known secret's file is not held back by a template that waits (issue 1553)' (#398) from fix/1553-fill-file-by-file into main 2026-10-21 00:13:38 +00:00
jschoubben 3757d75d6a Wait for the background scan's outcome in the test instead of reading it once, and say where brother_health sits (issue 1552)
mesh/merge-gate head 3757d75d6acf merged into trunk 9aa97926141f — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head 3757d75d6acf merged into trunk 9aa97926141f — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 02:08:50 +02:00
mesh-tools f99f11e8a4 Merge pull request 'Mail reading: mesh-mail-reader notes each new mail, tells mail that matters on Telegram, and the mail page shows and corrects it (design 59 milestone 2)' (#397) from feat/mail-reading into main 2026-10-21 00:06:24 +00:00
jschoubben b7243505ca Carry brother's certificate check on a watch process beside the device unit check, as fail2ban does (issue 1510) 2026-10-21 02:05:21 +02:00
jschoubben 7eddb116b1 Answer brother_health's wait for unset settings with the SDK's health.Waiting (issue 1510) 2026-10-21 02:00:54 +02:00
mesh-tools 37035dfc90 Fill a module's files one by one, so a known secret's file is not held back by a template that waits (issue 1553)
mesh/merge-gate head 37035dfc904b merged into trunk f99f11e8a406 — pass: builds mesh-provisioner-client → ace, g14, novox, shanks; no bus step; every machi…
mesh/repo-check head 37035dfc904b merged into trunk f99f11e8a406 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 00:00:09 +00:00
jschoubben 65e67abb7d Bound the certificate check to 3 s a handshake and 7 s, and answer waits while printer is not given (issue 1510) 2026-10-21 01:58:50 +02:00
jschoubben 95c47a09c9 Print from a path only under the home folder, refused in one fixed way (issue 1551) 2026-10-21 01:57:34 +02:00
jschoubben 43771458f6 Cut several documents apart only when scanner.scan is asked apart: true (issue 1528) 2026-10-21 01:56:52 +02:00
jschoubben ef833e26c7 Drop an ask taken up after its deadline, follow a scan for its run, re-arm open documents and delete their pages (issues 1539, 1552, 1538) 2026-10-21 01:56:24 +02:00
jochen f3007eb242 Fix the try on 8000b637 and review round 2: RunBatches takes its gap as a parameter instead of reading the var BatchGap, and no tell while unhealthy is checked as an exact count, not a before/after that a same-pass tell could hide behind (issue 1520, 1524)
mesh/merge-gate head f3007eb242f8 merged into trunk 6c6d5f681c30 — pass: builds mesh-mail, mesh-mail-web, new: modules/mesh-mail-reader → novox; no bus ste…
mesh/repo-check head f3007eb242f8 merged into trunk 6c6d5f681c30 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 01:52:58 +02:00
jochen 8000b63785 Review round 1: an unset mail-reading verb answers unavailable not unknown, cache the index instead of leaking a pool per call, a gone mail leaves the queue without going unhealthy, sort the judge's own cause by its words, drive RunBatches itself for its pacing test, and prove no tell while unhealthy against a genuinely untold mail (issues 1430, 1523, 1517, 1518, 1520, 1519, 1524) 2026-10-21 01:48:23 +02:00
jschoubben 1e19e8ee9a Judge the module healthy by the certificates the printer serves against the pinned list (issue 1510) 2026-10-21 01:47:25 +02:00
jschoubben a75e11e814 Scan several glass pages into one PDF with scanner.scan document start, an id and finish (issue 1538) 2026-10-21 01:45:53 +02:00
jschoubben 17f3c45182 Cut each of several documents lying apart on the glass out and file it as its own PDF (issue 1528) 2026-10-21 01:44:05 +02:00
jschoubben a5bc37459b Let printer.print take a file path and upload and print it in one call (issue 1551) 2026-10-21 01:41:21 +02:00
jschoubben 23c3a2396e Answer a long feeder scan as started with an id, file it in the background, and scale the feeder's bound with its pages (issues 1552, 1539) 2026-10-21 01:40:29 +02:00
jschoubben 7bf63f9eff Ask a failed part fetch once more before a scan fails, and name the stored object (issue 1536) 2026-10-21 01:37:26 +02:00
jschoubben 8295bc3894 Refuse a redirect off https or off the printer's host and a re-sent login body in the admin client (issue 1515) 2026-10-21 01:36:59 +02:00
jochen f89b73d936 Fix the try on 63980bf5: keep a test database's name within postgres's 63-byte limit so two calls in one long-named test never collide (issue 1517) 2026-10-21 01:30:52 +02:00
jochen 63980bf5ac Merge remote-tracking branch 'origin/main' into feat/mail-reading 2026-10-21 01:25:15 +02:00
jochen cfea9de530 Fix the try on bb594652: a const batch gap unit tests assert directly, wait for the bundle's own subscribe before publishing, and a migration-0001 database built through the real path (issues 1520, 1518, 1431, 1517) 2026-10-21 01:24:57 +02:00
mesh-tools 6c6d5f681c Merge pull request 'Send the scan request on one line so the Brother honours it, and scan from the feeder, both sides, into one PDF (issue 1452)' (#396) from fix/1452-feeder-duplex into main 2026-10-20 23:19:56 +00:00
jochen bb594652fa Merge remote-tracking branch 'origin/main' into feat/mail-reading 2026-10-21 01:12:06 +02:00
jochen 794de09c4d Fix the try: guard shared test state with mutexes, pgxpool over one conn, a short batch gap for integration builds, unique+forced database drops (issues 1431, 1524, 1517) 2026-10-21 01:11:48 +02:00
mesh-tools d62c2edc35 Merge pull request 'Mark the build modules moved, so a machine assigned them composes before the forge serves their entry (issue 1537)' (#395) from fix/1537-build-modules-moved into main 2026-10-20 23:09:36 +00:00
jschoubben d5f9cd465c Send brother's scan settings on one line and scan from the feeder, both sides, into one PDF (issue 1452)
mesh/merge-gate head d5f9cd465c96 merged into trunk d62c2edc35f3 — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head d5f9cd465c96 merged into trunk d62c2edc35f3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The Brother ignores the whole body when it holds a line break, so every setting was ignored and the glass was scanned at its default.
2026-10-21 01:05:55 +02:00
mesh-tools c7e892ca63 Mark the build modules moved, so a machine assigned them composes before the forge serves their entry (issue 1537)
mesh/merge-gate head c7e892ca63b3 merged into trunk 82649bc770d1 — pass: builds mesh-build-go, mesh-build-node, sent nowhere; no bus step; every machine co…
mesh/repo-check head c7e892ca63b3 merged into trunk 82649bc770d1 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-20 22:59:36 +00:00
jochen d61fe4e2da Merge remote-tracking branch 'origin/main' into feat/mail-reading 2026-10-21 00:56:07 +02:00
jochen 3d030c9b4f Prove a refused judgement fails loudly and catches up, and the page shows why (issue 1524) 2026-10-21 00:55:23 +02:00
jochen 95704c7410 Start mesh-mail-reader whole against the real bus and store: entries, no body kept, the tell, the cap (issue 1431) 2026-10-21 00:53:37 +02:00
mesh-tools 82649bc770 Merge pull request 'Close every eSCL scan job brother opens, and say a busy scanner as busy (issue 1531)' (#392) from fix/1531-close-scan-jobs into main 2026-10-20 22:53:09 +00:00
mesh-tools a28398c16b Merge pull request 'File a scan of any size: answer its parts in 128 KiB under the state's 256 KiB value limit, and end only a too-large answer at once (issue 1535)' (#394) from fix/1535-scan-answer-size into main 2026-10-20 22:47:14 +00:00
jochen 0bc59c6f4a Correct a mail's mark or group and undo a lesson from the page, with the weekly measure (issue 1523) 2026-10-21 00:46:56 +02:00
jschoubben 66616ed353 Bring fix/1531 up to main, so its check is judged against main again (issue 888)
mesh/merge-gate head 66616ed3538f merged into trunk a28398c16be6 — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head 66616ed3538f merged into trunk a28398c16be6 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 00:42:56 +02:00
jochen 8ff94287a7 Show mail that matters, mail groups and scam warnings above the classical list (issue 1430) 2026-10-21 00:39:31 +02:00
mesh-tools 3811ac69ed Merge pull request 'Let printer.settings say each setting's current value: the checked radio, a checkbox's state, the selected option (issue 1530)' (#393) from fix/1530-settings-current-values into main 2026-10-20 22:38:06 +00:00
jschoubben db581abb05 Terminate an ask only for an answer the state refuses as too large; retry any other failed answer write (issue 1535)
mesh/merge-gate head db581abb0570 merged into trunk 3811ac69ed3e — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head db581abb0570 merged into trunk 3811ac69ed3e — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 00:35:26 +02:00
jochen 35496d6381 Send the mail signal after each batch: messenger_check, tell, untold carried over (issue 1522) 2026-10-21 00:34:52 +02:00
jschoubben 61796cd3dd Answer a scan's parts in 128 KiB, under the state's 256 KiB value limit, and end an ask whose answer is refused instead of redelivering it (issue 1535) 2026-10-21 00:33:00 +02:00
jochen 72f4bc52a4 Build the mail signal's own words: headline, nearest due date first (issue 1429) 2026-10-21 00:31:14 +02:00
jochen e87269b0d2 Keep the operator's corrections as reading lessons at once: correct, lessons, undo (issue 1428) 2026-10-21 00:27:47 +02:00
jochen a13a6c5898 Serve entries, groups and status: filtered, sorted, with the weekly reading-lessons measure (issue 1521) 2026-10-21 00:22:37 +02:00
jochen 42bffb9363 Run the reader's batches: paced, judged, written as entries, unhealthy on failure (issue 1520) 2026-10-21 00:18:37 +02:00
mesh-tools 43e0daf694 Merge pull request 'Name the build test's fixture manifest module.json.fixture, so no reader takes it for a module of the catalogue (issue 1527)' (#391) from fix/1527-fixture-manifest-name into main 2026-10-20 22:15:31 +00:00
jschoubben 6d7117a361 Never guess a label from another control's, form's or link's text; test the radio group with nothing checked (issue 1530)
mesh/merge-gate head 6d7117a361b8 merged into trunk 43e0daf6947d — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head 6d7117a361b8 merged into trunk 43e0daf6947d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-21 00:11:48 +02:00
jschoubben 3df52a8ac5 Answer each settings field's current value, and leave the menu links and hidden fields out unless asked (issue 1530) 2026-10-21 00:08:58 +02:00
jochen 83bc01e746 Build the batch's question and read its answer, bounded and out of shape when wrong (issue 1427) 2026-10-21 00:08:28 +02:00
jschoubben eac59c60c8 Say a Platen job's second page as an error instead of dropping it (issue 1531)
mesh/merge-gate out of date: judged against trunk 199f72fed29d, the trunk is now 43e0daf6947d; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 199f72fed29d, the trunk is now 43e0daf6947d; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
2026-10-21 00:05:34 +02:00
jschoubben a760a4695a Close every eSCL scan job brother opens, so the next scan is not refused with 503 (issue 1531) 2026-10-21 00:03:28 +02:00
jochen 183cc58347 Keep mesh-mail-reader's own health, surviving a restart (issue 1519) 2026-10-20 23:58:37 +02:00
mesh-tools b56670535f Name the build test's fixture manifest module.json.fixture, so no reader takes it for a module of the catalogue (issue 1527)
mesh/merge-gate head b56670535f82 merged into trunk 199f72fed29d — pass: builds fixture-build, sent nowhere; no bus step; every machine composes with the c…
mesh/repo-check head b56670535f82 merged into trunk 199f72fed29d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery stopped: a merged change never reached the machines: its walk plan-1792534533713829068 ended done before its word without building fixture-…
2026-10-20 21:56:31 +00:00
jochen d7b5c87887 Hear mesh-mail's added events, recorded before acknowledged, in arrival order (issue 1518) 2026-10-20 23:56:17 +02:00
jochen 0d2d19daa0 Keep a mail entry per mail, never the body (issue 1517) 2026-10-20 23:53:48 +02:00
jochen d17b7e832d Declare the seat mail-reading and the module mesh-mail-reader's skeleton (issue 1426) 2026-10-20 23:50:02 +02:00
mesh-tools 199f72fed2 Merge pull request 'The Go and the Node build modules, ssh-client on the SDK's key keeper, and gitea's read-only builder accounts (design 47 R4c-3)' (#389) from feat/r4c3-build-modules into main 2026-10-20 21:43:14 +00:00
mesh-tools 06d5dc9830 Merge pull request 'Start brother's admin login where the printer serves its form, and never leave a session open or post the password off https (issue 1514)' (#390) from fix/1514-admin-login-start-page into main 2026-10-20 21:28:42 +00:00
jschoubben 02076ebe93 Log in to the default page and ask for the page after, so a missing page leaves no session; send the password over https only (issue 1514 review)
mesh/merge-gate head 02076ebe9351 merged into trunk e2e990a0ebdb — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head 02076ebe9351 merged into trunk e2e990a0ebdb — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-20 23:21:15 +02:00
mesh-tools bcee61edf7 Skip, loudly, a build module's real-toolchain test where the check's image lacks the tool; it is proven live on a build node (issues 1486, 1490)
mesh/repo-check head bcee61edf709 merged into trunk 06d5dc98303a — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/merge-gate head bcee61edf709 merged into trunk 06d5dc98303a — pass: group feat/r4c3-build-modules composed: every machine composes with the change as …
mesh/delivery failed: its walk failed, carrying its own commit: a gate on a first machine (what it carried put back), a build, a machine
mesh/delivery-group group feat/r4c3-build-modules failed: a member failed
2026-10-20 21:19:15 +00:00
jschoubben 2716c6faed Name the captured root response 2026-10-20 23:18:39 +02:00
jschoubben b1950f6a5c Start the printer's admin login at the root and default to /home/status.html: this firmware has no /general/status.html behind the login (issue 1514) 2026-10-20 23:18:36 +02:00
mesh-tools 983a6c39c8 Ship the Node build module's toolchain lock as an archive, not a template naming hosts (issue 1488) 2026-10-20 21:15:56 +00:00
mesh-tools 00fa5e74d0 Review round 1 for the Node build module: registry for other @novox packages and refused bundles (1489), toolchain.reported stream (1483, 1488), re-pin to mesh-sdk 36d6f774; delta part 4 of 4 2026-10-20 21:12:10 +00:00
mesh-tools 4a1804cda3 Review round 1 for the Node build module: build depth (1490), bin scripts for the test script (1491), toolchain.reported and its stream (1483, 1488); delta part 3 of 4 2026-10-20 21:10:18 +00:00
mesh-tools 7e7cc27c30 Review round 1 for the checks, ssh-client and the Go build module: sshdir (1484), socket path (1486), over_bound (1290, 1486), toolchain.reported and streams (1483), re-pin to mesh-sdk 36d6f774; delta part 2 of 4 2026-10-20 21:09:36 +00:00
mesh-tools bb5d420538 Bring the branch up to main: brother pins several printer certificates (issue 1503), messenger offers unsent asks again (issue 1507), delta part 1 of 4 2026-10-20 21:08:08 +00:00
mesh-tools bda49fb945 Test the Node build module's fetch, forge files, manifest and guard (issues 1489, 1484, 1488), part 19 of 19 2026-10-20 21:05:55 +00:00
mesh-tools e2e990a0eb Merge pull request 'Offer an open ask no channel has sent again on the minute tick: a stopped messenger loses its outbox (issue 1507)' (#388) from fix/1507-offer-unsent-asks into main 2026-10-20 21:05:01 +00:00
mesh-tools 1d6c905407 Add the Node build module's workspace and the tests of its build and test steps (issues 1490, 1491), part 18 of 19 2026-10-20 21:04:12 +00:00
mesh-tools 59e6f53a12 Add the Node build module's fetch, test step, registry and toolchain install (issues 1488, 1489, 1491), part 17 of 19 2026-10-20 21:02:50 +00:00
mesh-tools 97ed21c250 Add the Node build module's toolchain lock, main, recipe, guard and key keeper (issues 1488, 1484), part 16 of 19 2026-10-20 21:01:26 +00:00
mesh-tools 3f6f8909d9 Add the Node build module's manifest, ssh files, toolchain package, build step and toolchain tests (issues 1488, 1484, 1490), part 15 of 19 2026-10-20 20:59:51 +00:00
mesh-tools c27f978bc4 Add the Go build module's fixture repositories, and correct the copied build recipe (issues 1290, 1291, 1486, 1487), part 14 of 20 2026-10-20 20:57:30 +00:00
jschoubben 7bd7a34090 Offer an open ask no channel was sent again on the minute tick: the outbox is lost when the process stops (issue 1507)
mesh/merge-gate head 7bd7a34090b2 merged into trunk f1906d9ffa6c — pass: builds messenger → novox; no bus step; every machine composes with the change as i…
mesh/repo-check head 7bd7a34090b2 merged into trunk f1906d9ffa6c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-20 22:57:13 +02:00
mesh-tools 9436ef8777 Add the Go build module's captured go output and fixture repositories (issues 1290, 1291, 1486, 1487), part 13 of 20 2026-10-20 20:57:00 +00:00
mesh-tools dbc36d3d3c Test the Go build module's integration level against a faked and a real store and bus (issues 1486, 1290, 1487), part 12 of 20 2026-10-20 20:56:12 +00:00
mesh-tools 2462f96f65 Add the Go build module's test step with its unit tests and the guard tests (issues 1290, 1483), part 11 of 20 2026-10-20 20:54:52 +00:00
mesh-tools 86d79f7872 Add the Go build module's digest step, its tests and the level judging (issues 1291, 1290, 1486), part 10 of 20 2026-10-20 20:53:50 +00:00
mesh-tools be7f04dbd4 Test the Go build module's fetch, forge files and manifest (issues 1485, 1484, 1483, 1482), part 9 of 20 2026-10-20 20:52:35 +00:00
mesh-tools 36c933541f Add the Go build module's fetch, build and integration level steps, and the build tests (issues 1485, 1486, 1487), part 8 of 20 2026-10-20 20:51:21 +00:00
mesh-tools 6cf74414e9 Add the Go build module's main, recipe, guard, key keeper, runner, store and bus, and its README (issues 1483, 1484, 1485, 1486), part 7 of 20 2026-10-20 20:50:11 +00:00
mesh-tools f1906d9ffa Merge pull request 'Let brother's printer-certificate pin several fingerprints: the printer serves an ECDSA and an RSA certificate (issue 1503)' (#387) from fix/1503-printer-two-certificates into main 2026-10-20 20:49:33 +00:00
mesh-tools 298a0b8eda Test the forge account provider's builder entries, and add the Go build module's manifest, ssh files and README (issues 1481, 1483), part 6 of 17 2026-10-20 20:48:52 +00:00
mesh-tools fb52442b50 Serve a build module's key from its own state, read-only on nox-builder, in the forge account provider (issue 1482), part 5 of 17 2026-10-20 20:47:25 +00:00
mesh-tools 5702046667 Make the builders team and the read-only nox-builder keys in the forge account library, and watch the build modules' keys (issues 1481, 1482), part 4 of 17 2026-10-20 20:46:23 +00:00
mesh-tools 8dc0bc98bd Move ssh-client's tests and dependencies onto the SDK's forgekey (issue 1480), part 3 of 17 2026-10-20 20:45:35 +00:00
mesh-tools bf8f47ce66 Let ssh-client use the SDK's forgekey, and let the checks accept a module's own .ssh (issues 1480, 1484), part 2 of 17 2026-10-20 20:44:30 +00:00
mesh-tools 1e1def4d32 Remove ssh-client's own key keeper, key parser and runner, now in the SDK's forgekey (issue 1480), part 1 of 17 2026-10-20 20:43:51 +00:00
jschoubben 6312bc4219 Let printer-certificate pin several fingerprints, since the printer serves an ECDSA and an RSA certificate (issue 1503)
mesh/merge-gate head 6312bc421964 merged into trunk c294f7bd16dc — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head 6312bc421964 merged into trunk c294f7bd16dc — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-20 22:43:02 +02:00
mesh-tools c294f7bd16 Merge pull request 'Make the messenger's hourly cap a setting, messages-per-hour: 100 by default, 0 turns the pause off (issue 1476)' (#386) from fix/1476-messenger-cap into main 2026-10-20 20:21:56 +00:00
mesh-tools 59f8c9be00 Merge pull request 'Let brother's scan-folder take an absolute path: mesh-config matches a pattern against the whole value (issue 1472)' (#385) from fix/brother-scan-folder-pattern into main 2026-10-20 20:17:25 +00:00
jschoubben a5d3480e7e Let messages-per-hour 0 turn the hourly pause off (issue 1476)
mesh/merge-gate head a5d3480e7e5d merged into trunk 59f8c9be0090 — pass: builds messenger → novox; no bus step; every machine composes with the change as i…
mesh/repo-check head a5d3480e7e5d merged into trunk 59f8c9be0090 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-20 21:54:25 +02:00
jschoubben dccae4b0e7 Make the messenger's per-channel hourly cap the setting messages-per-hour, default 100 (issue 1476) 2026-10-20 21:54:01 +02:00
jschoubben 35aac9f358 Let brother's scan-folder take an absolute path: mesh-config matches a pattern against the whole value (issue 1472)
mesh/merge-gate head 35aac9f358cd merged into trunk 1357023bed09 — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head 35aac9f358cd merged into trunk 1357023bed09 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-20 21:49:47 +02:00
mesh-tools 1357023bed Merge pull request 'The brother module: one module for the Brother printer, holding the scanner and printer seats, scans kept in its bucket and printing over the broker (issue 1453, ADR 0467, ADR 0468)' (#384) from feat/brother into main 2026-10-20 19:48:06 +00:00
jschoubben 2f6086120c issue 1453: read the fake printer's connection count under its lock, which the race detector requires; say 700 KiB as ADR 0468 does
mesh/merge-gate head 2f6086120c46 merged into trunk 809dd76973fd — pass: builds scan, new: modules/brother, sent nowhere; no bus step; every machine compos…
mesh/repo-check head 2f6086120c46 merged into trunk 809dd76973fd — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery stopped: a merged change never reached the machines: its walk plan-1792525688362824501 ended done before its word without building scan (mo…
2026-10-20 21:39:27 +02:00
jschoubben 02cb424d5c brother: emits declared in the module's own list too, parts of 700 KiB at most, a duplicates window on device-asks, and the built binary ignored (issue 1453) 2026-10-20 21:37:07 +02:00
jschoubben b430b35633 brother: README for the bucket, the documents in parts, printing and the device process (issue 1453) 2026-10-20 21:31:06 +02:00
jschoubben 1d62305001 brother: scanning moves into the device process and its scans are kept in the bucket; scanner.document, printer.upload and printer.print carry documents in parts and print over IPP (issue 1453, ADR 0468) 2026-10-20 21:30:51 +02:00
jschoubben d6e88fac17 brother: the module requires an s3-bucket, with mesh-mail's S3 client and store.json read by the device account alone (issue 1453, ADR 0468 clause 2) 2026-10-20 21:24:19 +02:00
jschoubben feada9fb14 brother: the admin process becomes the device process, brother-device, with device-asks and device-answers (issue 1453, ADR 0468 clause 1) 2026-10-20 21:23:28 +02:00
jschoubben adfb61c053 brother: the admin account, its owned secrets, the process and the printer-asks stream declared as mesh-mail's sender is; an integration test and the README (issue 1453) 2026-10-20 21:21:15 +02:00
jschoubben 29107b0d93 brother: the admin process alone reads the admin password and logs in; printer.settings queues an ask and reads its answer (issue 1453) 2026-10-20 21:20:16 +02:00
jschoubben 58c0c0682c brother: README for the two seats, the pinned certificate, the admin password and the login (issue 1453) 2026-10-20 21:12:56 +02:00
jschoubben 6782816707 brother: the printer seat's status verb, IPP Get-Printer-Attributes by hand and the eSCL state, replayed from captures (issue 1453) 2026-10-20 21:12:45 +02:00
jschoubben 34260d8c29 brother: the printer seat's settings verb, read-only through the login, with the page's links and form fields (issue 1453) 2026-10-20 21:11:15 +02:00
jschoubben 2b62a1819e brother: the printer config and admin-password secret, HTTPS pinned to one certificate, and the login by the form's own field (issue 1453) 2026-10-20 21:10:14 +02:00
jschoubben 78f9731d4a brother: the scanner seat declared and claimed, its verbs scan, status and list; preferences untrusted (issue 1453, 1446) 2026-10-20 21:07:00 +02:00
jschoubben 4a892ee29d Move the scan module to brother, ADR 0467 clause 1: the code unchanged in what it does (issue 1453) 2026-10-20 21:06:24 +02:00
mesh-tools 809dd76973 Merge pull request 'A scan module: scan the glass over eSCL, crop the document, file it as a PDF and announce it (issue 1440, ADR 0464)' (#383) from feat/scan-module into main 2026-10-20 18:23:43 +00:00
jschoubben 0821718d7c issue 1440: the scan module's manifest, settings template and README
mesh/merge-gate head 0821718d7ca9 merged into trunk a67f01963f0d — pass: builds new: modules/scan, sent nowhere; no bus step; every machine composes with t…
mesh/repo-check head 0821718d7ca9 merged into trunk a67f01963f0d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-20 20:13:58 +02:00
jschoubben 27709b07e7 issue 1440: scan_now, scan_status and scan_list, and scan.added said after the rename 2026-10-20 20:13:02 +02:00
jschoubben ae47a6cc10 issue 1440: file the scan as a one-page PDF, written whole and renamed, and list the filed scans 2026-10-20 20:11:39 +02:00
jschoubben 26ae4d2fd8 issue 1440: the plain-lid test's specks lie where counting them would make a box 2026-10-20 20:10:29 +02:00
jschoubben cf407f187d issue 1440: crop the document out of the platen's white 2026-10-20 20:10:20 +02:00
jschoubben 36a34a49b8 issue 1440: the scan module's eSCL client, settings and image density 2026-10-20 20:09:47 +02:00
mesh-tools a67f01963f Merge pull request 'mesh-issues stops listening to the removed module's transition, which every controller merge check now refuses (issue 1406)' (#382) from fix/1406-drop-the-old-transition-name into main 2026-10-20 13:28:29 +00:00
mesh-tools 56ba1c3837 Hear the gate's transition alone: today's module is forgotten (issue 1406)
mesh/merge-gate head 56ba1c3837e5 merged into trunk db32f8ac5632 — pass: builds mesh-delivery-gate, mesh-issues → novox; no bus step; every machine compose…
mesh/repo-check head 56ba1c3837e5 merged into trunk db32f8ac5632 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-20 13:02:35 +00:00
mesh-tools db32f8ac56 Merge pull request 'Remove today's delivery module now that the gate holds its seat (design 47 R3e, issue 1351, ADR 0454)' (#381) from fix/1351-remove-todays-delivery-module into main 2026-10-20 12:27:48 +00:00
mesh-tools 1c7024224d Make the catalogue test strict: the gate alone claims the seat, and say the old module is gone (ADR 0454, issue 1351)
mesh/merge-gate head 1c7024224dd7 merged into trunk bb0fe93bbddd — pass: builds mesh-delivery, mesh-issues → novox; no bus step; every machine composes wit…
mesh/repo-check head 1c7024224dd7 merged into trunk bb0fe93bbddd — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-20 12:03:46 +00:00
mesh-tools 1ef19030f1 Remove today's mesh-delivery module: the gate holds the seat since the handover (ADR 0454, issue 1351) 2026-10-20 12:02:56 +00:00
mesh-tools bb0fe93bbd Merge pull request 'The gate takes over today's mesh-delivery: mesh-delivery-gate holds the seat, asks and merges through the parts, and today's module leaves (design 47 R3e, issues 1346-1351)' (#380) from feat/r3e-the-gate into main 2026-10-20 06:49:17 +00:00
mesh-tools 1d9c72c476 Compare in the carry bed what the carry copies, not what the gate does after it; keep today's module among mesh-issues' owners
mesh/repo-check head 1d9c72c476f8 merged into trunk a3a9b8749d12 beside mesh-tools#37@06e6721e — pass: its merge-check.sh passed, each part in its toolchai…
mesh/merge-gate head 1d9c72c476f8 merged into trunk a3a9b8749d12 — pass: group feat/r3e-the-gate composed: every machine composes with the change as it did…
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1792499270059758752 (novox/mesh-catalog at db32f8ac into main,…
mesh/delivery-group group feat/r3e-the-gate delivered: every member is delivered
2026-10-20 06:43:26 +00:00
mesh-tools 1f193f6d01 Let the catalogue hold the gate and today's module as the two claimants of the seat, and say the removal follows the handover (issue 1351)
mesh/delivery-group group feat/r3e-the-gate checking: 2 of 3 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate waits for its group's composed check
mesh/repo-check head 1f193f6d01d2 merged into trunk a3a9b8749d12 beside mesh-tools#37@06e6721e — fail: its merge-check.sh failed: --- FAIL: TestTheGateCarr…
2026-10-20 06:43:17 +00:00
mesh-tools 41f18b2452 Keep today's module mesh-delivery in the catalogue, unchanged: its removal follows the handover as a pull request of its own
mesh/merge-gate waits for its group's composed check
mesh/repo-check head 41f18b24527f merged into trunk a3a9b8749d12 beside mesh-tools#37@06e6721e — fail: its merge-check.sh failed: --- FAIL: TestTheGateCarr…
mesh/delivery-group group feat/r3e-the-gate rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
2026-10-20 06:41:48 +00:00
mesh-tools cf5c02737b Pin mesh-sdk to main's merge of #47 (88581d0a), the same tree as ba684e54 the review read
mesh/merge-gate head cf5c02737bdb merged into trunk a3a9b8749d12 — fail: builds mesh-delivery, mesh-issues, new: modules/mesh-delivery-gate → novox; no bus…
mesh/repo-check head cf5c02737bdb merged into trunk a3a9b8749d12 — fail: its merge-check.sh failed: --- FAIL: TestTheGateCarriesALiveCapturesDeliveriesAndG…
mesh/delivery-group group feat/r3e-the-gate rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
2026-10-20 06:32:31 +00:00
mesh-tools a3a9b8749d Merge pull request 'Let a commit copy files from another repository on the forge, so a large move reaches the forge from any machine (issue 1365)' (#379) from fix/1365-a-commit-copies-from-a-repository into main 2026-10-20 05:57:00 +00:00
mesh-tools 83adc46fe0 Give each asking at unchanged heads its own router id, overwrite the ask instead of taking it away, and announce the answered asking (review round 1, G2; issue 1370) 2026-10-20 01:26:28 +00:00
mesh-tools 67fe9a4dbe Pin mesh-sdk at issue 1370's commit and read the phase removed by its name (review round 1) 2026-10-20 01:25:52 +00:00
mesh-tools 1a985d68fd Test that the carry copies a group, a pull key and an unasked delivery byte for byte (review round 1) 2026-10-20 01:20:37 +00:00
mesh-tools 68117bd74d Hold the seat only when no assignment stands, every have key says removed and both watches are current; fix the two bed tests that failed the try (review round 1, G1) 2026-10-20 01:20:09 +00:00
mesh-tools dc3a177410 Let a commit copy files from another forge repository, so a large move is not typed through the caller
mesh/merge-gate head dc3a177410fa merged into trunk c0880c4dc271 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head dc3a177410fa merged into trunk c0880c4dc271 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-20 01:07:28 +00:00
mesh-tools 3b03c220ad Check that the catalogue holds no module mesh-delivery but through the names its handover needs, and say how the seat passes (issue 1351) 2026-10-20 00:59:36 +00:00
mesh-tools d16fa1e052 Hear the gate's transitions beside today's delivery module's in mesh-issues, and name the gate among the owners (issue 1350) 2026-10-20 00:58:01 +00:00
mesh-tools 6a39da5485 Owe the forge its statuses, views, notes and comments as keys, read the plan from plans as data, and call an empty check answer an error (issue 1349) 2026-10-20 00:52:42 +00:00
mesh-tools beafa55d97 Ask the Approve through the gate's asks, hand merges to the bridge one member at a time, and move source.main once a group is merged (issue 1348) 2026-10-20 00:45:02 +00:00
mesh-tools dd462a80e2 Keep the gate silent until it holds the seat, and carry today's state over once when it first does (issue 1347) 2026-10-20 00:29:19 +00:00
mesh-tools a390bb40f8 Take a stray key out of the gate's reads (issue 1346) 2026-10-20 00:19:29 +00:00
mesh-tools 09f0b0d5ab Move the delivery module to the gate: mesh-delivery-gate claims the seat with its own buckets and reads (issue 1346) 2026-10-20 00:19:19 +00:00
mesh-tools c0880c4dc2 Merge pull request 'Read the grant file on keycloak's moved path, so a consumer not moved gets its client (issue 1332)' (#378) from fix/1332-keycloak-reads-the-grant-file into main 2026-10-19 22:46:05 +00:00
jochen b9104454f9 Read the grant file on keycloak's moved path, so a consumer not moved is still made (issue 1332)
mesh/merge-gate head b9104454f9a5 merged into trunk 954f84ea19b5 — pass: builds keycloak, sent nowhere; no bus step; 1 wait(s) for a person; every machine …
mesh/repo-check head b9104454f9a5 merged into trunk 954f84ea19b5 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh-mail-web, grafana and car-hunter are not moved, so keycloak wanted nothing for them: it never
made mesh_novox_mail_web, and held mesh_ace_grafana and mesh_ace_carhunt for retirement. Keycloak's
moved spec now asks for the grant file's consumers too (WithGrants: true), and the moved harness
reads MESH_RECEIVES exactly as the grant path does and as postgres's moved harness already did.
2026-10-20 00:30:45 +02:00
jochen 73a10fafc7 Pull keycloak's moved-path harness into movedHarness, and test it reads the grant file (issue 1332)
Adds the failing unit tests for keycloak's provider on the moved path: a grant-file consumer not
moved should still be made and never held for retirement, and the moved harness should read
MESH_RECEIVES as the grant path does. All three fail on an assertion against today's code, which
wants nothing from the grant file on this path.
2026-10-20 00:30:10 +02:00
mesh-tools 954f84ea19 Merge pull request 'The mail web client: mesh-mail-web, and mesh-mail's views for it (design 59 milestone 1)' (#377) from feat/mail-webclient into main 2026-10-19 21:27:33 +00:00
jochen 9f3a8ee391 Follow real links instead of building them, and check the CSP on every answer (issue 1268)
mesh/merge-gate head 9f3a8ee39101 merged into trunk 66aa204f6779 — pass: builds mesh-mail, new: modules/mesh-mail-web → novox; no bus step; every machine c…
mesh/repo-check head 9f3a8ee39101 merged into trunk 66aa204f6779 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-19 23:18:53 +02:00
jochen cad6194447 Keep the HTTP endpoint's own state, so status and health stop claiming to serve once it has failed (issue 1283) 2026-10-19 23:18:51 +02:00
jochen 3fd1b47fe7 Bounce once through this origin before sign-in, so a Strict cookie is not lost crossing from Keycloak or another site (issue 1263) 2026-10-19 23:18:48 +02:00
jochen 5b57422523 Report an unknown ask through a small interface, not *testing.T directly (issue 1285)
A failed subtest fails its parent, so TestHarnessFailsTheTestWhenTheBundleAsksSomethingUnknown could never
pass the way it was written (try 2's finding). The player now reports through askFailer (Helper, Errorf);
every other test keeps passing its own *testing.T, which satisfies it already. That one test gives a
recorder instead, and asserts it holds exactly one message naming seat:mail-store.accounts, and that
mail-webclient.status's own answer shows the ask got an error back. Checked: no other integration test uses
the same deliberate-subtest-failure pattern (grepped t.Run).
2026-10-19 22:53:18 +02:00
jochen 523d7846cd Fix mesh-mail-web's manifest: operators is type list, the listen word names its port number (issue 1262)
The controller refused the manifest: config.operators declared type "array" (not one of the controller's
five types, novox/hq ADR 0348 §4 — the word is "list"), and the bundle's env word MESH_MAIL_WEB_LISTEN said
${port:web} (a bundle's word takes the port NUMBER a listens entry gives, novox/hq ADR 0192; mesh-controller
internal/catalogue/port_into.go portsFilledInto matches it against listens[].Port, never an endpoint's
label). Fixed to type "list" and ${port:8099} (the listens entry's own port). main.go and the integration
harness already read/write a plain port number, so neither needed a change. manifest_test.go now asserts
both: config.operators.Type == "list", and the listen word equals ${port:<the listens entry's port>};
confirmed each independently fails against the old values before this fix.
2026-10-19 22:47:56 +02:00
jochen 502cda34cb Add design 59 milestone 1's acceptance tests on the runtime player (issue 1268)
Four scenarios, entered only through HTTP with Host mail.<node>.internal as a browser would, against the
built mesh-mail-web, a stand-in identity provider (auth_test.go's own fakeIssuer, reused) and a real account
bucket holding granted account a and ungranted account c: sign-in redirects, 403s and the session cookie's
attributes; the 404 for another host; browsing and a send followed to sent, only the five mail verbs asked,
account c never shown, paging by date, no database or extra writable directory; a mail's script and remote
image shown as text, an attachment downloaded with nosniff, and the CSP on every answer.

Build tag integration; compiles under go vet -tags integration ./...; never run here.
2026-10-19 22:42:27 +02:00
jochen c996121e9d Add the runtime player and stand-in mail-store for a bundle's integration test (issue 1285)
Plays the SDK's own runtime protocol (initialize, tools/list, tools/call, the bundle's mesh/ask and
mesh/state.<verb>) against mesh-mail-web's real built binary, from stdio.go and state.go's own source. The
account bucket is a real JetStream key-value bucket (mesh-mail_accounts, named as mesh-mail's own
acceptance_integration_test.go names a module's state); its fixture (accountBucketValue/accountBucketFolder)
is accounts.go's own, itself copied from mesh-mail's mirror.go AccountValue (issue 1255, 1264).

Build tag integration; compiles under go vet -tags integration ./...; never run here (go test -tags
integration is this batch's hard rule) — the lead's try runs it.
2026-10-19 22:38:36 +02:00
jochen f863e852c9 Add the write page and a send's state, behind issue 1284's form token (issue 1267)
Wires 1284's per-session token into the real write form and its real POST /send; adds tests for 1284/AC1-AC4
against that real page and handler, beside this issue's own AC1-AC4.
2026-10-19 22:29:41 +02:00
jochen 58b337770d Show a mail as escaped text and serve its attachments as downloads (issue 1266) 2026-10-19 22:26:47 +02:00
jochen ff1886ac62 Add the search page, by mail-store.search's own argument names (issue 1265) 2026-10-19 22:24:52 +02:00
jochen 28fdbbd346 Show every granted account, its state and its folders' counts (issue 1264)
Accounts bucket fixture (accountBucketValue/accountBucketFolder) copied from mesh-mail's own
modules/mesh-mail/cmd/mesh-mail/mirror.go AccountValue/AccountFolderValue (issue 1255); mesh-mail-web is its
own Go module, so this is a copy, not an import.
2026-10-19 22:22:52 +02:00
jochen 3ee80c98ab Bind every changing form's token to its session (issue 1284) 2026-10-19 22:18:03 +02:00
jochen 8e0ef84060 Sign in through Keycloak: code flow with PKCE, verified ID token, 12-hour session (issue 1263)
Library choice: golang.org/x/oauth2 for the code exchange, github.com/coreos/go-oidc/v3 for the ID token's
verification (issuer, audience, expiry, signature from the issuer's JWKS).
2026-10-19 22:16:15 +02:00
jochen 68c141849f Serve mesh-mail-web's HTTP endpoint: internal host check, CSP, open (issue 1283) 2026-10-19 22:09:49 +02:00
jochen a9ad306693 Add the module mesh-mail-web, with its status verb (issue 1262) 2026-10-19 22:07:36 +02:00
jochen a3e9985c15 Keep each account's newest mails in a bucket, bounded past 81 accounts (issue 1282) 2026-10-19 21:56:04 +02:00
jochen 08200bc178 Keep each account's state in a bucket (issue 1255) 2026-10-19 21:50:12 +02:00
jochen a9ff48e200 Answer a send's state only to its caller (issue 1261) 2026-10-19 21:44:12 +02:00
mesh-tools 66aa204f67 Merge pull request 'Say a group member refused only because main moved a warning for ten minutes, urgent after (issue 1275 AC6, ADR 0442)' (#376) from fix/1275-ac6-out-of-date-warning into main 2026-10-19 18:58:21 +00:00
jschoubben a76910a0c0 Say only the merge tool's two out of date refusals a warning, and never step a severity down (issue 1275, ADR 0442's cases)
mesh/merge-gate head a76910a0c036 merged into trunk 9efdd87a73a9 — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head a76910a0c036 merged into trunk 9efdd87a73a9 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-19 20:51:58 +02:00
jschoubben dc86203d29 Say a refusal for an out of date verdict a warning for ten minutes, urgent after (issue 1275, operator's amendment, AC6) 2026-10-19 20:51:58 +02:00
mesh-tools 9efdd87a73 Merge pull request 'Merge a group whole or not at all, and say loudly when it cannot (issues 1275, 1276)' (#375) from fix/1275-group-merges-whole-or-not into main 2026-10-19 18:45:31 +00:00
jschoubben 301cd2287f Answer gitea_commit_statuses in the bus test's runtime, and say a held, refused member once (issue 1275, review round 1)
mesh/merge-gate head 301cd2287f5f merged into trunk eda55ceb9e1b — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head 301cd2287f5f merged into trunk eda55ceb9e1b — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-19 20:38:48 +02:00
jschoubben 524e3405e5 Ask a group's Approve only when every member may merge, and withdraw the ask when a required status turns (issue 1275)
mesh/merge-gate head 524e3405e517 merged into trunk eda55ceb9e1b — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head 524e3405e517 merged into trunk eda55ceb9e1b — fail: its merge-check.sh failed: --- FAIL: TestAGroupOfTwoWithGreenChecksGetsOneAskAndOn…
mesh/delivery superseded: a newer head of the same pull request
An Approve given hours after its ask must not rest on a check made at the ask: a required status
that turns not a success while the ask waits takes the ask back, and its late answer merges nothing.
2026-10-19 20:32:22 +02:00
jschoubben 6e6d1f3e47 Merge a group whole or not at all: re-read every required status first, stop at the first refusal, say a held or half merged group urgent (issue 1275, 1276)
An approved group was merged member by member, each on its own, so a refusal of the first
did not stop the second. A group now merges in one pass in its order, after every member's
required statuses are read again at its head, and a held or half merged group stays raised.
A refusal naming the member's own repository check as missing asks that check (issue 1276).
2026-10-19 20:32:15 +02:00
mesh-tools eda55ceb9e Merge pull request 'Give the forge bridge a merge commit's trees in one call, and each open pull request's head branch (design 47 R3c, issue 1234)' (#374) from feat/delivery-parts into main 2026-10-19 18:22:09 +00:00
mesh-tools e8c3448797 Merge pull request 'Answer each gmail mail once when no folder is named, keeping archived mail (issue 1248)' (#373) from fix/1248-gmail-mail-once into main 2026-10-19 13:39:15 +00:00
jochen 5c1ff4bb15 mesh-mail: dedup-archive picks the same copy in both stores when a message-id also sits in a second non-archive folder (issue 1248, review round 1 finding 1)
mesh/merge-gate head 5c1ff4bb152b merged into trunk b767aac8e072 — pass: builds mesh-mail → novox; no bus step; every machine composes with the change as i…
mesh/repo-check head 5c1ff4bb152b merged into trunk b767aac8e072 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-19 15:33:36 +02:00
jochen 280aeb2f47 Answer each gmail mail once when no folder is named, keeping archived mail (issue 1248) 2026-10-19 15:30:44 +02:00
mesh-tools b767aac8e0 Merge pull request 'mail_search answers the operator's questions: optional words, filters by sender, recipient, cc, date, words, attachment, flags, thread and correspondents, ordered and grouped (issues 1228-1232)' (#372) from feat/mail-search into main 2026-10-19 13:17:13 +00:00
jochen 920199862e mesh-mail: group-by-sender and order-by sender compare addresses case-insensitively in Postgres too, and a sender's shown name and address are its newest mail's, deterministically (issue 1230, review finding 6)
mesh/merge-gate head 920199862e18 merged into trunk 7bb588ca1bfb — pass: builds mesh-mail → novox; no bus step; every machine composes with the change as i…
mesh/repo-check head 920199862e18 merged into trunk 7bb588ca1bfb — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-19 15:09:44 +02:00
jochen 8dd2a42448 mesh-mail: mail_search's order defaults to descending when left out, matching its own schema text, and AC1's no-filter test proves ordering, limit and grant isolation together (issue 1232, review findings 5 and 7) 2026-10-19 15:05:23 +02:00
jochen d1a9056a1c mesh-mail: mail_search gains words/words-any, a word found in subject or body, since subject-words and body-words together only ever meant and (issue 1229, review finding 3; also proves AC9's type-or-name independently and AC2+unread combined) 2026-10-19 15:02:40 +02:00
jochen a3476a7bf2 mesh-mail: from-someone-i-wrote-to recognises gmail's Sent folder name too, and stays scoped to one account (issue 1231, review findings 1 and 2) 2026-10-19 14:57:26 +02:00
jochen 1c3158e098 mesh-mail: mail_search's address filter never treats % or _ as a wildcard, so a literal domain filter does not leak every mail in Postgres (issue 1228, review finding 4) 2026-10-19 14:52:36 +02:00
jschoubben 75957fc3c0 Hash the root tree from the root's entries, since the forge names the commit (issue 1234)
mesh/repo-check head 75957fc3c006 merged into trunk e8c3448797c7 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/merge-gate head 75957fc3c006 merged into trunk e8c3448797c7 — pass: group feat/delivery-parts composed: every machine composes with the change as it d…
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1792433168676870614 (novox/mesh-catalog at eda55ceb into main,…
mesh/delivery-group group feat/delivery-parts delivered: every member is delivered
The listing's top sha is the commit's, not the root tree's (seen live, novox/mesh-delivery at 7f61817), so
gitea_commit_trees now computes the root tree as git does, from the entries without a slash. The query keys
recursive and per_page stay in the allow-list: read flags only.
2026-10-19 14:52:29 +02:00
jschoubben ea83746297 Give the forge bridge a merge commit's trees in one call, and a pull request's head branch (issue 1234)
gitea_commit_trees reads the commit's recursive tree listing and answers the root tree and the
tree of every directory holding a module.json; a cut listing fails loudly. gitea_open_pulls
entries gain head. The query keys recursive and per_page join the allow-list, read flags only.
mesh-delivery decodes open pulls with json.Unmarshal into a struct (cmd/mesh-delivery/ports.go:405-410),
which ignores the added key.
2026-10-19 14:50:19 +02:00
jochen b60ee706d4 mesh-mail: mail_search's tool makes the query optional and offers every filter and order-by (one schema for mail_search and mail-store.search), validated before the index is ever asked (issue 1232) 2026-10-19 14:46:58 +02:00
jochen 4a60deb4a8 mesh-mail: mail_search finds a thread's replies and mail from someone the operator wrote to, so a reply or a known correspondent's junk mail can be found (issue 1231) 2026-10-19 14:39:59 +02:00
jochen fc10031d22 mesh-mail: mail_search orders by date, sender, subject or size and groups by sender, so the newest, the largest or who writes most can be answered (issue 1230)
Query's From becomes a list, OR'd (1228/AC11's several domains), since 1230's order-by needs it to test the
ascending-order criterion end to end; To and Cc stay single, no criterion asking for more.
2026-10-19 14:36:53 +02:00
jochen 2985122856 mesh-mail: mail_search's Query gains subject/body words, attachment type/name and read/unread, so a word in one field, an attachment or a flag can filter mails (issue 1229) 2026-10-19 14:29:15 +02:00
jochen 20342f2172 mesh-mail: mail_search's Query gains from, to, cc and a date range, so a sender, a recipient or a date can filter mails without query words (issue 1228) 2026-10-19 14:22:14 +02:00
mesh-tools 7bb588ca1b Merge pull request 'mesh-mail sends an approved mail from its own sender process, and tells a failed send once (ADR 0436, ADR 0438; issues 1172, 1193-1199)' (#371) from feat/mail-sender into main 2026-10-19 11:36:51 +00:00
jochen a2c0c58677 mesh-mail: a redelivery of a message whose send already finished (sent, failed, refused or not proven) is a repeat, terminated without recording, emitting or telling anything again (operator-approved change to ADR 0436 clause 10; issue 1196)
mesh/merge-gate head a2c0c586772b merged into trunk c441d83ba753 — pass: builds mesh-mail → novox; no bus step; every machine composes with the change as i…
mesh/repo-check head a2c0c586772b merged into trunk c441d83ba753 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-19 13:29:31 +02:00
mesh-tools c441d83ba7 Merge pull request 'Keep two values of each issue in the tracker's bucket, since the log holds them all (issue 1221)' (#370) from fix/1221-tracker-keeps-two-copies into main 2026-10-19 11:08:47 +00:00
jschoubben e601e88e95 Keep two values of each issue in the tracker's bucket, since the log holds them all (issue 1221)
mesh/merge-gate head e601e88e95dd merged into trunk fb8e677cd849 — pass: builds mesh-issues → novox; no bus step; every machine composes with the change as…
mesh/repo-check head e601e88e95dd merged into trunk fb8e677cd849 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-19 12:38:55 +02:00
mesh-tools fb8e677cd8 Merge pull request 'The router takes a module's condition on the operator channel's one subject (design 47 R3b: issues 1217, 1218)' (#369) from feat/delivery-seat-by-claim into main 2026-10-19 10:28:28 +00:00
jschoubben 5aca005cb7 Shorten the router's burst window in the test bundle so the desk is sent within seconds (issue 1218, check finding 1)
mesh/repo-check head 5aca005cb741 merged into trunk b4527d9091e9 beside mesh-controller#394@3e7f34d0 — pass: its merge-check.sh passed, each part in its to…
mesh/merge-gate head 5aca005cb741 merged into trunk b4527d9091e9 — pass: group feat/delivery-seat-by-claim composed: every machine composes with the change…
mesh/delivery delivered
mesh/delivery-group group feat/delivery-seat-by-claim delivered: every member is delivered
2026-10-19 12:15:47 +02:00
jschoubben 9f9e9fc9d3 Shorten the router's start grace in the test bundle so the bus tests stay in seconds (issue 1218, review round 1 finding 2) 2026-10-19 12:05:07 +02:00
jschoubben f737418bb3 Keep a module's condition open when the controller's conditions are read (issue 1217, review round 1 finding 1) 2026-10-19 12:04:58 +02:00
jschoubben 5b235a6e80 Prove over the bus that a condition on the one subject reaches the desk (issue 1218) 2026-10-19 11:59:49 +02:00
jschoubben de6741a335 Take a module's condition on the operator channel's one subject (issue 1217) 2026-10-19 11:58:10 +02:00
jochen dd8305d97d mesh-mail: the refused-sign-in acceptance test's wait excluded queued too, so it returned done on the very first look before the sender ever claimed anything (issue 1198) 2026-10-19 11:10:36 +02:00
jochen 06dfce87f5 mesh-mail: decide kept by settleSend's own returned revision, not its error, so a changed revision is actually retried instead of silently accepted as another writer's state; emit mail.send.failed from claimQueuedSend only once its own write lands (review round 2; issue 1198) 2026-10-19 11:07:24 +02:00
jochen 7db4bd215d mesh-mail: fix the work-queue stream's real bus name (MAIL_SENDS, not MAIL-SENDS) and build the integration tests' stream, subject and duplicate window exactly as the controller composes them, exercising the send id's dedup (review round 1 finding 7; issue 1198) 2026-10-19 10:57:45 +02:00
jochen 1639269ad4 mesh-mail: on Approve the tools record the send queued directly, with no separate approved state, then publish; a stuck queued record is republished by the sweep past a short grace, never NakWithDelay'd by the sender (ADR 0436 clause 8, decided to replace the review round 1 item 2 work-around; issues 1194, 1196, 1199) 2026-10-19 10:53:32 +02:00
jochen 7397051771 mesh-mail: tighten tests that proved less than their criterion — attempt 5 and no mail received (1198/AC4), mail_health actually turning unhealthy (1199/AC2), and the words mail_send_status gives, not only its status (1199/AC4) (review round 1 finding 6) 2026-10-19 10:53:12 +02:00
jochen 95089762ef mesh-mail: retry the outcome's own write on a changed revision, and never ack or emit an outcome that could not be kept (review round 1 finding 5; issue 1198) 2026-10-19 10:43:45 +02:00
jochen ce3b12f349 mesh-mail: emit mail.send.failed when the sender's own claim refuses or finds a send already sending, same as a failed send after it (review round 1 finding 4; issue 1198) 2026-10-19 10:40:41 +02:00
jochen acb82992ee mesh-mail: index a send for a tell or mail_health's overdue check before its record carries the outcome, drop the old index entry only after, and report an index write that fails instead of dropping it silently (review round 1 finding 3; issues 1194, 1199) 2026-10-19 10:38:07 +02:00
jochen 0a7669b48f mesh-mail: a send still approved (the tools' own publish-then-record race) is redelivered later, never refused or told (review round 1 finding 2; issue 1196) 2026-10-19 10:31:39 +02:00
jochen f140099a75 mesh-mail: declare the sender's own secret broker-send, so the controller can issue its account (review round 1 finding 1; issue 1172) 2026-10-19 10:29:48 +02:00
jochen f3a59ee63a mesh-mail: tell the operator once for a failed approved send and once more if its sender has not taken it, make an overdue send unhealthy, and give mail_send_status its full vocabulary (ADR 0436 clauses 11, 12; issue 1199) 2026-10-19 10:22:27 +02:00
jochen 33a97e6d13 mesh-mail: the sender records a send's outcome, retries a not-taken failure and emits mail.send.sent or mail.send.failed, acknowledging only once it is kept (ADR 0436 clauses 10, 11; issue 1198) 2026-10-19 10:11:41 +02:00
jochen e7dce9616c mesh-mail: classify an SMTP send's outcome (not taken, refused, not proven), and carry the send id in the Message-ID (ADR 0436 clause 10; issue 1197) 2026-10-19 10:03:44 +02:00
jochen 1ccb617a86 mesh-mail: the sender verifies the operator's warrant again from the router's own record and claims a send once before any connection (ADR 0436 clauses 9, 10; issue 1196) 2026-10-19 09:57:07 +02:00
jochen dd3ed68483 mesh-mail: a send process that connects on its own bus account and consumes the mail-sends queue, stopping cleanly on a signal (ADR 0436 clause 7; issue 1195) 2026-10-19 09:49:24 +02:00
jochen 428b42181d mesh-mail: on the operator's Approve the tools record approved and queue the send for the sender, instead of sending it themselves (ADR 0436 clause 8; issue 1194) 2026-10-19 09:44:42 +02:00
jochen 59c0906c79 mesh-mail: move password reading out of the tools' reach into its own package, so only keep and the send actor import it (ADR 0436 clause 12; issue 1193) 2026-10-19 09:32:54 +02:00
jochen 75a9a45c6f mesh-mail: pin mesh-sdk go to release 2, declare the sender account, bus process and work-queue stream (ADR 0436 clauses 7, 8; issue 1172) 2026-10-19 09:23:45 +02:00
mesh-tools b4527d9091 Merge pull request 'Ask a refused merge again when its checks turn green on the forge (issue 1144)' (#368) from fix/1144-merge-asked-again-on-green-status into main 2026-10-18 22:05:45 +00:00
jochen a32b7af3fd Ask a refused merge again when its checks turn green on the forge (issue 1144)
mesh/merge-gate head a32b7af3fd8a merged into trunk a6295d6b98d5 — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head a32b7af3fd8a merged into trunk a6295d6b98d5 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-18 23:50:25 +02:00
mesh-tools a6295d6b98 Merge pull request 'Write a provisioner client's files only on a change over the bus, never on a timer (issue 1129, ADR 0430)' (#366) from fix/1129-no-drift-pass into main 2026-10-18 16:56:32 +00:00
mesh-tools afb94d925e Merge pull request 'Register the repository mesh-delivery as an owner, so its issues can be located in it (issue 1133)' (#367) from fix/1133-mesh-delivery-owner into main 2026-10-18 16:52:06 +00:00
jschoubben 26d81b09b5 Register the repository mesh-delivery as an owner, so its issues can be located in it (issue 1133)
mesh/merge-gate head 26d81b09b5e8 merged into trunk 72ad5ad138ce — pass: builds mesh-issues → novox; no bus step; every machine composes with the change as…
mesh/repo-check head 26d81b09b5e8 merged into trunk 72ad5ad138ce — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The tracker runs with the manifest's owners default; a test now holds the
default and the template's copy to one list.
2026-10-18 18:39:11 +02:00
mesh-tools 72ad5ad138 Merge pull request 'A repository the gitea module makes refuses an administrator's merge past its checks (issue 1141)' (#365) from fix/1141-admin-override into main 2026-10-18 16:24:56 +00:00
jschoubben 54db38be08 gitea: a repository the module makes refuses an administrator's merge past its checks (issue 1141)
mesh/merge-gate head 54db38be0896 merged into trunk 4301da630d82 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 54db38be0896 merged into trunk 4301da630d82 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
narrowed never set block_admin_merge_override, so novox/mesh-delivery's
main let an administrator merge past mesh/merge-gate and mesh/repo-check,
unlike every other repository's main since 2026-10-07. narrowed sets it and
isRight requires it, so the module's next start puts mesh-delivery right.
2026-10-18 18:10:29 +02:00
jochen e7ff7c3045 Write a provisioner client's files only on a change over the bus, never on a timer (issue 1129, ADR 0349 clause 4)
mesh/merge-gate head e7ff7c3045fc merged into trunk afb94d925e94 — pass: builds mesh-provisioner-client → ace, g14, novox, shanks; no bus step; every machi…
mesh/repo-check head e7ff7c3045fc merged into trunk afb94d925e94 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-18 18:08:50 +02:00
mesh-tools 4301da630d Merge pull request 'gitea fills repositories.json through the provisioner client, so an approved repositories value lands at once (issue 1128)' (#364) from fix/1128-repositories-template into main 2026-10-18 16:03:58 +00:00
jschoubben e75e793a4b gitea: fill repositories.json through the provisioner client, so an approved value lands at once (issue 1128)
mesh/merge-gate head e75e793a4b3b merged into trunk cfb90d7b2f96 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head e75e793a4b3b merged into trunk cfb90d7b2f96 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The file was merged by the controller (merge, takes-config), which writes a
config change only on the node's next send (ADR 0348 §10): the operator's
Approve of the mesh-delivery repository set the value and nothing made it.
A plain template saying ${config:repositories} is filled by the provisioner
client on config-changed, and the node-engine reruns the run-once program
that names the file under restart-on.
2026-10-18 17:40:59 +02:00
mesh-tools cfb90d7b2f Merge pull request 'The gitea module makes the organisation's repositories from its config, protected before any team reaches them; no tool creates in the organisation (issue 1124, ADR 0428)' (#363) from fix/1124-repositories-config into main 2026-10-18 15:05:04 +00:00
jschoubben 11761637cf Put main's rule right again once a new repository is in the organisation: the forge drops a merger without access, and under the setup account the tools' account has none (issue 1124, ADR 0428 §3, review round 1)
mesh/merge-gate head 11761637cf81 merged into trunk e47d0694c752 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 11761637cf81 merged into trunk e47d0694c752 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-18 16:58:17 +02:00
jschoubben 8c587d0f00 Let the repositories pass's failed exit be its gate: a run-once step the node-engine runs again until it passes, its stderr naming each refused repository, step and forge answer, instead of a health check of its own (issue 1124, ADR 0428 §6) 2026-10-18 16:52:29 +02:00
jschoubben df275b3c6e Make the organisation's repositories from the gitea module's config value, protected under the setup account before they join the organisation, and take the owner argument from gitea_create_repo, since a repository's maker becomes its administrator (issue 1124, ADR 0428) 2026-10-18 16:49:12 +02:00
jschoubben 94b49a296f Test that the gitea module makes the organisation's repositories from its config, protected before any team reaches them, and that no tool creates in the organisation (issue 1124, ADR 0428) 2026-10-18 16:45:49 +02:00
mesh-tools e47d0694c7 Merge pull request 'gitea_create_repo creates a repository in an organisation, saying the forge's refusal (issue 1124)' (#362) from fix/1124-create-repo-in-org into main 2026-10-18 13:50:31 +00:00
jschoubben 0d3f0fd3fc Let gitea_create_repo create in an organisation, saying the forge's refusal, so novox repositories can be made through the tools (issue 1124)
mesh/merge-gate head 0d3f0fd3fc89 merged into trunk 7f77ce663a9e — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 0d3f0fd3fc89 merged into trunk 7f77ce663a9e — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-18 15:45:01 +02:00
jschoubben 50fee22d0a Test that gitea_create_repo creates in an organisation and says the forge's refusal (issue 1124) 2026-10-18 15:44:38 +02:00
mesh-tools 7f77ce663a Merge pull request 'mesh-delivery waits for nothing: a check is asked at once and a standing verdict adopted (issue 1115)' (#361) from fix/zero-delay into main 2026-10-18 10:38:05 +00:00
jschoubben 6954467292 mesh-delivery bus test waits for the group's composed check, not a member's own check asked at once (issue 1115)
mesh/merge-gate head 695446729264 merged into trunk 580fadb63ecb — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head 695446729264 merged into trunk 580fadb63ecb — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-18 12:33:02 +02:00
jschoubben 5f002441b7 mesh-delivery takes an empty answer to a check ask as a refusal (issue 1115)
mesh/merge-gate head 5f002441b7f0 merged into trunk 580fadb63ecb — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head 5f002441b7f0 merged into trunk 580fadb63ecb — fail: its merge-check.sh failed: --- FAIL: TestAGroupOfTwoWithGreenChecksGetsOneAskAndOn…
mesh/delivery superseded: a newer head of the same pull request
2026-10-18 12:28:03 +02:00
jschoubben fc9ddd9185 mesh-delivery adopts a standing verdict only when every required check stands, at every look, and counts an ask with no id as none (issue 1115) 2026-10-18 12:25:53 +02:00
jschoubben 1760b94824 mesh-delivery waits for nothing: a check is asked at once and a standing verdict adopted (issue 1115) 2026-10-18 12:22:31 +02:00
mesh-tools 580fadb63e Merge pull request 'A pull request at the same head as one closed unmerged takes its delivery over (issue 1114)' (#360) from fix/takeover-on-close into main 2026-10-18 09:59:02 +00:00
jschoubben 30d3bb9466 A pull request announced at a live head takes its delivery over when the other one closes or is edited (issue 1114)
mesh/merge-gate head 30d3bb94667f merged into trunk e40ec47058e6 — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head 30d3bb94667f merged into trunk e40ec47058e6 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-18 11:53:28 +02:00
mesh-tools e40ec47058 Merge pull request 'mesh-delivery hears a pull request closed unmerged from the forge's own event (issue 1107)' (#359) from fix/close-heard-from-forge-event into main 2026-10-18 09:00:18 +00:00
jschoubben bc1acddaa4 mesh-delivery hears the forge's own pull-request closed event as a close unmerged (issue 1107)
mesh/merge-gate head bc1acddaa4e5 merged into trunk 798d6fd68589 — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head bc1acddaa4e5 merged into trunk 798d6fd68589 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-18 04:55:48 +02:00
mesh-tools 798d6fd685 Merge pull request 'A group member refused for its own pending repo-check is asked its own check, not the composed one (issue 1049)' (#358) from fix/1049-own-check-on-refusal into main 2026-10-18 02:41:25 +00:00
jschoubben b0ec4f6f14 A group member refused for a pending status of its own asks its own check once, not the composed check (issue 1049)
mesh/merge-gate head b0ec4f6f1405 merged into trunk d3dd46a9fdb4 — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head b0ec4f6f1405 merged into trunk d3dd46a9fdb4 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-18 04:27:42 +02:00
mesh-tools d3dd46a9fd Merge pull request 'Readers of the assignments skip an ended or pending one (design 47 milestone 5, issue 1098)' (#357) from feat/chain-on-the-bus into main 2026-10-18 02:04:16 +00:00
jschoubben 3bc4215d87 Take the bus's backup guard out of modules/nats: it would move the bus (issue 1098, guard follows in issue 1104)
mesh/repo-check head 3bc4215d872c merged into trunk 44e7002d3c84 beside mesh-controller#378@0149a3d7 — pass: its merge-check.sh passed, each part in its to…
mesh/delivery stopped: its walk plan-1792289060772890935 was stopped by a person (mesh-controller): the operator, via telegram (user id verified), chose …
mesh/delivery-group group feat/chain-on-the-bus stopped: a member was stopped
mesh/merge-gate out of date: judged against trunk 798d6fd68589, the trunk is now e40ec47058e6; judged again on a recheck or when it is merged
2026-10-18 03:55:50 +02:00
jschoubben 68e3326d4c Providers and the provisioner client take the SDK that reads an ended assignment as a removed key (issue 1098)
mesh/repo-check head 68e3326d4cbc merged into trunk 44e7002d3c84 beside mesh-controller#378@b56c1d00 — pass: its merge-check.sh passed, each part in its to…
mesh/delivery-group group feat/chain-on-the-bus ready: every member ready, and composed together they pass
mesh/merge-gate head 68e3326d4cbc merged into trunk 44e7002d3c84 — pass: group feat/chain-on-the-bus composed: every machine composes with the change as it…
mesh/delivery superseded: a newer head of the same pull request
2026-10-18 02:40:18 +02:00
jschoubben 84af0ed180 Guard that the bus storage stays declared as data with a backup (issue 1098) 2026-10-18 02:37:28 +02:00
jschoubben 9edd16eaeb mesh-provisioner-client takes a module off the node when its assignment is ended (issue 1098) 2026-10-18 02:37:28 +02:00
jschoubben 60a3823f0e mesh-vault reads an ended assignment as a removed key (issue 1098) 2026-10-18 02:37:28 +02:00
jschoubben bdbca4b921 mesh-config drops an ended assignment as a removed one (issue 1098) 2026-10-18 02:37:28 +02:00
mesh-tools 44e7002d3c Merge pull request 'A group approved together merges in one pass and goes out in one delivery plan (design 47 milestone 3 fix: issues 1049, 1082, 1047, 1083, 1079)' (#356) from feat/group-one-pass into main 2026-10-17 23:44:21 +00:00
jschoubben 7383bc3170 A stale composed pass counts members judged since it was asked, whichever verdict lands first (issue 1047, review round 2)
mesh/merge-gate head 7383bc31703e merged into trunk 0c56f603059b — pass: builds gitea, mesh-delivery → novox; no bus step; every machine composes with the …
mesh/repo-check head 7383bc31703e merged into trunk 0c56f603059b — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-18 01:20:18 +02:00
jschoubben ebd29223f3 Prove over a real bus that an edit of the description asks the Approve again (issue 1079, review round 1) 2026-10-18 01:18:43 +02:00
jschoubben e8a74765f8 A composed check that lands judged against a trunk that has moved is out of date and asked again (issue 1047, review round 1) 2026-10-18 01:18:13 +02:00
jschoubben 6556e61485 Read a pull request's review record again when its description is edited, and ask the Approve again if it was unanswered (issue 1079) 2026-10-18 01:12:40 +02:00
jschoubben 93365be98f A new pull request at a commit whose delivery was stopped and unmerged takes that delivery over (issue 1083) 2026-10-18 01:10:51 +02:00
jschoubben 814c1a991c A trunk move asks at once the check of a group member whose Approve is not asked yet (issue 1047) 2026-10-18 01:10:12 +02:00
jschoubben 983e4a6444 A closed group's member whose merge was refused asks the composed check again at once, and a refusal loop past ten minutes is said (issue 1082) 2026-10-18 01:09:37 +02:00
jschoubben da094008ea A group's composed verdict names each member's siblings at the heads it composed, so a sibling's own merge is no move (issue 1049) 2026-10-18 01:08:26 +02:00
mesh-tools 0c56f60305 Merge pull request 'Let a proposed record be withdrawn (issue 1081)' (#355) from fix/1081-withdraw into main 2026-10-17 22:15:25 +00:00
jochen 684e8801c4 Take the ask back on the router when withdrawing a record (issue 1081, review round 1)
mesh/merge-gate head 684e8801c420 merged into trunk 3395af96bc34 — pass: builds mesh-knowledge → novox; no bus step; every machine composes with the change…
mesh/repo-check head 684e8801c420 merged into trunk 3395af96bc34 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
WithdrawRecord only ended the waiting ask locally; the router kept it open, so
the operator still saw Approve/Decline for a withdrawn record and an answer to
it was dropped silently. It now also publishes a cancel on the router's
subject once the withdraw is saved, as the controller's asker does; a cancel
the router cannot be told of is logged and the withdraw still stands. The
module's existing "uses": ["operator-channel"] already grants this; no manifest
change needed, since the ask and cancel subjects are both under that seat.
2026-10-18 00:09:16 +02:00
jochen 6394a62a02 Merge remote-tracking branch 'origin/main' into fix/1081-withdraw 2026-10-18 00:03:16 +02:00
jochen ddf7a5ab09 Let a proposed record be withdrawn (issue 1081)
A dropped proposal or a stale reservation stayed proposed forever: there was
no way to say the operator decided against it. knowledge-base.withdraw takes
a reserved or proposed record to withdrawn as a new version naming why, ends
a waiting operator's ask and clears a pending review in the same version,
and refuses an already accepted, superseded or withdrawn record. The number
stays spent either way.

The old Withdraw (the background job for reservations never proposed) is
renamed WithdrawStale to make room for the new, caller-driven WithdrawRecord.
2026-10-18 00:02:48 +02:00
mesh-tools 3395af96bc Merge pull request 'mesh-delivery holds a delivery whose backup failed and release asks again; restic records a backup that cannot start (issues 1067, 1068)' (#353) from feat/backup-before-send into main 2026-10-17 21:48:51 +00:00
jschoubben 4d03755c65 Bring the branch up to main after #354 (issue 1047: the trunk move asked no check)
mesh/repo-check head 4d03755c6598 merged into trunk ad80048c22d8 beside mesh-controller#375@91a7c1ea — pass: its merge-check.sh passed, each part in its to…
mesh/merge-gate head 4d03755c6598 merged into trunk ad80048c22d8 — pass: group feat/backup-before-send composed: every machine composes with the change as …
mesh/delivery-group group feat/backup-before-send stopped: a member was stopped
mesh/delivery delivered
2026-10-17 23:43:21 +02:00
mesh-tools ad80048c22 Merge pull request 'Tools to read the knowledge base and the issue tracker in fewer, smaller calls (issues 1064, 1069, 1070, 1071)' (#354) from feat/kb-tracker-tools into main 2026-10-17 21:34:20 +00:00
jochen bf3b3a5e36 Review round 1: list's pending ignores an ask that has ended, and its before cursor never cuts a page between a decision and research effort of the same number (issue 1064)
mesh/merge-gate head bf3b3a5e362b merged into trunk f076cadecedc — pass: builds mesh-issues, mesh-knowledge → novox; no bus step; every machine composes wi…
mesh/repo-check head bf3b3a5e362b merged into trunk f076cadecedc — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-17 23:09:46 +02:00
jochen 7bdb8f4782 Review round 1: list numbers combines search and before as AND, its limit defaults to the count asked and said names a cut; README names brief, numbers and worker (issues 1070, 1071) 2026-10-17 23:07:29 +02:00
jochen b387cddc7c Let the knowledge base answer its whole-set findings, whole or by record (issue 1069) 2026-10-17 23:01:22 +02:00
jochen 1cfbcf8e30 Let the knowledge base list its records by kind, status, topic or a flag (issue 1064) 2026-10-17 22:59:09 +02:00
jschoubben c898a937a1 Order the events of the failed-backup bus test: each after the module took the one before (issue 1068)
mesh/merge-gate waits for its group's composed check
mesh/repo-check head c898a937a1e3 merged into trunk f076cadecedc beside mesh-controller#375@a39d6ce7 — pass: its merge-check.sh passed, each part in its to…
mesh/delivery-group group feat/backup-before-send checking: 1 of 2 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
2026-10-17 22:57:44 +02:00
jschoubben 82693a441c A release the controller refuses to take leaves the delivery held for its failed backup (issue 1068)
mesh/merge-gate waits for its group's composed check
mesh/repo-check head 82693a441cf0 merged into trunk f076cadecedc beside mesh-controller#375@a39d6ce7 — fail: its merge-check.sh failed: --- FAIL: TestAPlan…
mesh/delivery-group group feat/backup-before-send rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
2026-10-17 22:53:43 +02:00
jochen 1124f17912 Let issue-tracker.list take numbers and worker (issue 1070) 2026-10-17 22:50:09 +02:00
jschoubben 4799f79242 Read the plan's backups as the controller says them: machine, module, state, words (issue 1068) 2026-10-17 22:45:46 +02:00
jochen 01c9ab0cf5 Let issue-tracker.get answer briefly, with only its last note (issue 1071) 2026-10-17 22:45:42 +02:00
jschoubben 8f7aeeeb81 Hold a delivering delivery for a failed backup before the send; release asks the backups again (issue 1068) 2026-10-17 22:45:03 +02:00
jschoubben 914099774e Record a backup that cannot start as the module's last night (issue 1067) 2026-10-17 22:42:21 +02:00
mesh-tools f076cadece Merge pull request 'No ready mark: the Approve is asked once the checks pass, and a group merges in one pass on its one Approve (issues 1022, 978)' (#352) from feat/one-group-one-plan into main 2026-10-17 20:11:06 +00:00
jschoubben 0771797431 Merge remote-tracking branch 'origin/main' into feat/one-group-one-plan
mesh/merge-gate head 0771797431ed merged into trunk f69f84b9e54c beside mesh-controller#374@2d24fbb7 — pass: builds gitea, mesh-delivery → novox; no bus st…
mesh/repo-check head 0771797431ed merged into trunk f69f84b9e54c beside mesh-controller#374@2d24fbb7 — pass: its merge-check.sh passed, each part in its to…
mesh/delivery delivered
mesh/delivery-group group feat/one-group-one-plan delivered: every member is delivered
2026-10-17 21:40:08 +02:00
mesh-tools f69f84b9e5 Merge pull request 'Run the integration-tagged tests in the merge check, before any test carries the tag (issue 895)' (#351) from fix/895-catalog-script-first into main 2026-10-17 19:38:15 +00:00
jschoubben 47e42806ff Leave a composed member's gate to the group's verdict, reword the mark leftovers, restore main's merge-check.sh (issues 1022, 978) 2026-10-17 21:33:03 +02:00
jschoubben 86d5706182 Run the integration-tagged tests in the merge check, before any test carries the tag (issue 895)
mesh/delivery delivered
mesh/merge-gate head 86d5706182fd merged into trunk f69f84b9e54c — pass: the change touches no module of the mesh's graph
mesh/repo-check head 86d5706182fd merged into trunk f69f84b9e54c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
2026-10-17 21:32:23 +02:00
jschoubben 2d3b61ec45 Merge a group in one pass on its one Approve, with no order and no turns (issue 978)
A group has no order: mesh-delivery asks the controller for none, reads no after:
line (it says once that the line is not read), merges every member in one pass
once the composed check passed and the Approve covers the heads, and gives the
plan its word once every member is merged. A failed member stops no other. The
composed verdict sets each member's mesh/merge-gate (pending, saying why, until
one stands), the groups answer says composed_gate, and a trunk move asks the
out-of-date members' checks at once and merges on the same Approve (issue 1047).
2026-10-17 20:54:16 +02:00
jschoubben 668444785d Ask the Approve as soon as the checks pass, with no ready mark (issue 1022)
The review finishes before the push and its record is a Reviewed-by line in the
description, so the mark repeated what the push meant. mesh-delivery asks the
operator when a pull request's checks pass, keeps an ask through a trunk move
by asking the check again at once, and hands the merge tool the approved heads,
which replace the forge review of each sibling.
2026-10-17 20:44:54 +02:00
mesh-tools 5e7567e5a0 Merge pull request 'Every directory says how it treats files it does not know, and the catalogue checks it (issue 1024)' (#349) from feat/owned-paths into main 2026-10-17 18:14:12 +00:00
mesh-tools 7104df6fa2 Take in main's move of build-agent and route-proxy to mesh-controller (issue 1024)
mesh/merge-gate head 7104df6fa284 merged into trunk 5e6f5b8f6d99 — pass, with a note: builds artifact-store-tools, asus-zephyrus-g14, audit-logger, baserow…
mesh/repo-check head 7104df6fa284 merged into trunk 5e6f5b8f6d99 — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery delivered
mesh/delivery-group group feat/owned-paths delivered: every member is delivered
mesh-catalog #350 removed both modules from the catalogue; this change's edits to them go with them, and mesh-issues keeps both #350's change and its `unknown`. The branch's tree is the reviewed tree merged with main 5e6f5b8f6d.
2026-10-17 17:55:07 +00:00
mesh-tools 5e6f5b8f6d Merge pull request 'build-agent and route-proxy leave for mesh-controller (issue 1019, ADR 0420)' (#350) from feat/whole-modules into main 2026-10-17 17:44:16 +00:00
jschoubben 629aafdea2 Check again after the group's delivery stopped (issue 1019)
mesh/merge-gate head 629aafdea205 merged into trunk 54b3ce503e79 — pass: builds mesh-issues, new: modules/build-agent, modules/route-proxy → novox; no bus …
mesh/repo-check head 629aafdea205 merged into trunk 54b3ce503e79 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery covered after its stop: a later walk that contains it was delivered: plan-1792260860441586760 (novox/mesh-catalog at 5e7567e5 into main, do…
mesh-controller #370 merged and registered route-proxy from mesh-controller; its plan then built
route-proxy from the catalogue, which still held it, and failed. This member removes it there.
2026-10-17 19:36:31 +02:00
jschoubben 795806efa7 Declare the group's order: after novox/mesh-controller (issue 1019)
mesh/merge-gate waiting in the build queue since 19:32: asked by the merge tool against trunk 54b3ce503e79
mesh/repo-check waiting in the build queue since 19:32: asked by the merge tool against trunk 54b3ce503e79
mesh/delivery stopped: its group feat/whole-modules stopped: novox/mesh-controller@874a91a3c64f failed
mesh/delivery-group group feat/whole-modules delivering: 1 of 2 delivered; novox/mesh-catalog@795806efa7ad, stopped for a member delivered since, waits for a l…
mesh-delivery reads a pull request's after: lines only when it records a new head, so the
line added to #350's description takes effect with this head.
2026-10-17 18:43:08 +02:00
mesh-tools 652b00b25b Say on every directory how it treats files it does not know, and check it (issue 1024, ADR 0422)
mesh/merge-gate checking again: the gate refused `unknown` because the controller judged with a node-engine validator pinned before mesh-host #117; mesh-co…
mesh/repo-check checking again: the gate refused `unknown` because the controller judged with a node-engine validator pinned before mesh-host #117; mesh-co…
mesh/delivery-group group feat/owned-paths checking: 0 of 2 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
Every directory and archive resource carries `unknown`: move-out for a home folder whose program writes nothing there (the node-engine, delivered in mesh-host #117, moves the rest to ~/.local/state/mesh-moved/<module>/ with a list), module for ~/.ssh and ~/.claude, program elsewhere; `knows` keeps what a program or the operator relies on (powerlevel10k's compiled files, i3's scripts and the asus/g14 drop-ins, dunst's slack rule, betterbird's records, node-env's forge credential, issue 1033). Two shared folders are no longer declared. Each module's README says it. checks/owned holds every manifest to it; checks/sshdir stays while main's merge check runs it (issue 1035). Tests that pin a manifest follow the new key only. Reviewed in three rounds.
2026-10-17 16:23:03 +00:00
jschoubben c47d949258 Repoint the build-agent and route-proxy owners to mesh-controller (issue 1019)
mesh/delivery-group group feat/whole-modules rejected: its order contradicts itself: novox/mesh-catalog@c47d94925811 and novox/mesh-controller@48f5a7d3a99a: bu…
mesh/merge-gate checking again: issue 1043: mesh-controller #370's new head 0125ccd7 lets a member's own gate see the move through the clones beside it; th…
mesh/repo-check checking again: issue 1043: mesh-controller #370's new head 0125ccd7 lets a member's own gate see the move through the clones beside it; th…
mesh/delivery superseded: a newer head of the same pull request
2026-10-17 18:16:40 +02:00
jschoubben 4c6e0b625e Build-agent and route-proxy leave for mesh-controller (issue 1019) 2026-10-17 18:16:10 +02:00
mesh-tools 54b3ce503e Merge pull request 'mesh-delivery announces each merge with the Approve it merged on (issue 1025, ADR 0420)' (#348) from feat/move-a-module into main 2026-10-17 16:00:29 +00:00
jschoubben 40eed3a036 Merge remote-tracking branch 'origin/main' into feat/move-a-module
mesh/merge-gate head 40eed3a036db merged into trunk 0d2fc5e4d79c — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head 40eed3a036db merged into trunk 0d2fc5e4d79c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/delivery-group group feat/move-a-module delivered: every member is delivered
2026-10-17 17:19:44 +02:00
mesh-tools 0d2fc5e4d7 Merge pull request 'ssh-client's sweep keeps what ssh reads itself and linked key folders, and fails loudly (issue 1020, ADR 0417)' (#347) from fix/1020-sweep-edges into main 2026-10-17 15:13:46 +00:00
jschoubben 3ec2881a27 issue 1025: the merged announcement is an owed effect, and a group's merge is tested 2026-10-17 15:41:43 +02:00
jschoubben 4c0ed75090 issue 1025: mesh-delivery announces each merge on the Approve as the event merged, with its warrant 2026-10-17 15:40:00 +02:00
mesh-tools 05e6b583e2 Keep what ssh reads itself and linked key folders in ~/.ssh, and fail the sweep loudly (issue 1020, ADR 0417)
mesh/merge-gate head 05e6b583e2e3 merged into trunk ce56b491cffd — pass: builds ssh-client → ace, g14, novox, shanks; no bus step; every machine composes w…
mesh/repo-check head 05e6b583e2e3 merged into trunk ce56b491cffd — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery delivered
The sweep from issue 1017 moved known_hosts2, rc and environment, which ssh
and sshd read by default, and moved a link to a folder of keys, breaking every
login through it. It could move a file a tool was still writing. When ~/.local
was on another file system, every sweep failed at its first move and left an
empty folder each round, seen only in the journal. Its list was written after
the moves with errors ignored.

Now those files and linked key folders stay; a link whose target, or the folder
it names, cannot be checked is left and named; a copy of authorized_keys is
moved at any depth; a path changed in the last minute, at any depth, waits for
the next sweep; a retired folder on another file system fails the sweep before
anything is made; the list names each move before it is made; and each sweep's
outcome is kept for ssh_client_check, which shows a failed sweep as a problem.
A key's first line is read only so far.

The catalogue's new checks/sshdir refuses any manifest but ssh-client naming a
path under ~/.ssh (ADR 0415 §6). The README cites §7 for config.d.
2026-10-17 13:36:27 +00:00
mesh-tools ce56b491cf Merge pull request 'ssh-client keeps ~/.ssh to what it knows: sweep every other path into a kept folder (issue 1017, ADR 0415)' (#346) from fix/1017-retire-ssh-leftovers into main 2026-10-17 13:07:05 +00:00
mesh-tools 9675babf52 Merge pull request 'mesh-delivery acts on events and keeps each pull request's key; the forge's announcements carry a message id (issues 975, 976, design 47 milestone 2)' (#345) from feat/events-and-queues into main 2026-10-17 12:52:42 +00:00
jschoubben 94f65cccd9 issue 975: an empty commit so mesh-delivery reads the group's after: line, which it reads only when a head is announced (the operator's word, 2026-10-17)
mesh/merge-gate head 94f65cccd9c1 merged into trunk a7d526838ab0 beside mesh-controller#368@0f21fe78 — pass: builds build-agent, gitea, mesh-delivery → ace…
mesh/repo-check head 94f65cccd9c1 merged into trunk a7d526838ab0 beside mesh-controller#368@0f21fe78 — pass: its merge-check.sh passed, each part in its to…
mesh/delivery delivered
mesh/delivery-group group feat/events-and-queues delivered: every member is delivered
2026-10-17 14:39:21 +02:00
mesh-tools acf075fa63 ssh-client keeps ~/.ssh to what it knows: sweep every other path into a kept folder (issue 1017, ADR 0415)
mesh/merge-gate head acf075fa63cc merged into trunk 9675babf528b — pass: builds ssh-client → ace, g14, novox, shanks; no bus step; every machine composes w…
mesh/repo-check head acf075fa63cc merged into trunk 9675babf528b — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
ssh-client owns ~/.ssh, but treated every file it did not place as found, so a
predecessor's drop-in and old copies stayed there for weeks. Its process now
sweeps at start and every ten minutes: keys at any depth (and links to them),
authorized_keys, authorized_keys2 and sockets stay, and a folder holding one is
gone into; everything else moves to ~/.local/state/ssh-client/retired/<UTC
time>/, a folder never shared with another sweep, with a list beside it, which
ssh_client_check shows. Nothing is deleted.
2026-10-17 12:38:16 +00:00
jschoubben 52f70f42b7 issue 975: a merge heard again changes nothing, and a verdict naming no trunk commit is not written to the key
mesh/delivery-group group feat/events-and-queues rejected: its order contradicts itself: novox/mesh-catalog@52f70f42b749 and novox/mesh-controller@0f21fe7888be…
mesh/merge-gate head 52f70f42b749 merged into trunk a7d526838ab0 beside mesh-controller#368@0f21fe78 — pass: builds build-agent, gitea, mesh-delivery → ace…
mesh/repo-check head 52f70f42b749 merged into trunk a7d526838ab0 beside mesh-controller#368@0f21fe78 — pass: its merge-check.sh passed, each part in its to…
mesh/delivery superseded: a newer head of the same pull request
2026-10-17 14:10:14 +02:00
jschoubben add91ff048 issue 976: the forge's announcements get a deterministic message id; the announcement files go, the poll's watermark is kept in the module's state 2026-10-17 13:52:47 +02:00
jschoubben 2c8c1543ba issue 975: the pull request's key written by compare-and-set, the ready flag on a ready mark's check, build-agent reads the key 2026-10-17 13:48:54 +02:00
jschoubben 27ca8bbcff issue 975: the acceptance over a real bus: a ready mark and its Approve are merged within seconds of their events 2026-10-17 13:28:21 +02:00
jschoubben 0809a1f982 issue 975: a test that fails on a ticker and on a timer that names no bounded step it retries 2026-10-17 13:25:33 +02:00
jschoubben b5225d3a2f issue 975: no forge poll; the open pull requests are read at start and when a merge check's status, a new head or a ready mark says they changed 2026-10-17 13:25:03 +02:00
jschoubben d675f5e7c6 issue 975: no tick and no reconcile loop; a look after each event, one timer for the steps that wait on the clock 2026-10-17 13:23:25 +02:00
jschoubben 95e11464fe issue 975: ready marks, verdicts and Approves ask the merge as events, not at the next minute 2026-10-17 13:19:01 +02:00
mesh-tools a7d526838a Merge pull request 'One forge user, nox-agent, for every machine's agents' SSH pushes, each machine's key titled by machine (issue 991, ADR 0410)' (#344) from fix/991-one-forge-agent-user into main 2026-10-17 00:51:08 +00:00
mesh-tools 3a2560dc51 One forge user, nox-agent, for every machine's agents' SSH pushes, each machine's key titled by machine (issue 991, ADR 0410)
mesh/merge-gate head 3a2560dc51fc merged into trunk 82df3bd58913 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 3a2560dc51fc merged into trunk 82df3bd58913 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-17 00:46:43 +00:00
mesh-tools 82df3bd589 Merge pull request 'Give the forge an ssh name of its own, so it no longer merges with the operator's entry for its machine (issue 955, ADR 0409)' (#343) from fix/955-forge-ssh-name into main 2026-10-17 00:05:57 +00:00
mesh-tools 661b83249c Give the forge an ssh name of its own, so it no longer merges with the operator's entry for its machine (issue 955, ADR 0409)
mesh/merge-gate head 661b83249c2a merged into trunk bb291b5ad8af — pass: builds gitea, ssh-client → ace, g14, novox, shanks; no bus step; every machine com…
mesh/repo-check head 661b83249c2a merged into trunk bb291b5ad8af — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-17 00:01:42 +00:00
mesh-tools bb291b5ad8 Merge pull request 'Keep gitea's forge-account provider running when a watch is refused, so a later grant is picked up (issue 955)' (#342) from fix/955-provider-keeps-running into main 2026-10-16 23:30:28 +00:00
mesh-tools b159efd656 Keep gitea's forge-account provider running when a watch is refused, so a later grant is picked up (issue 955)
mesh/merge-gate head b159efd65673 merged into trunk 522780d5fcfa — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head b159efd65673 merged into trunk 522780d5fcfa — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-16 23:26:01 +00:00
mesh-tools 522780d5fc Merge pull request 'Let gitea's forge-account provider read the assignments it learns its consumers from (issue 955)' (#341) from fix/955-gitea-reads-assignments into main 2026-10-16 23:11:16 +00:00
mesh-tools 065ecd85f5 Let gitea's forge-account provider read the assignments it learns its consumers from (issue 955)
mesh/merge-gate head 065ecd85f53e merged into trunk 734a77f5a91a — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 065ecd85f53e merged into trunk 734a77f5a91a — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-16 23:05:10 +00:00
mesh-tools 734a77f5a9 Merge pull request 'Feature agents-write-git, step 2: git over SSH with a key made on each machine (issue 955)' (#340) from feat/agents-write-git-ssh into main 2026-10-16 22:31:17 +00:00
mesh-tools ae9cda8531 Hand ssh-client's forge files to the provisioner client (issue 955)
mesh/merge-gate head ae9cda8531b3 merged into trunk 017b729bc95d — pass: builds gitea, ssh-client → ace, g14, novox, shanks; no bus step; every machine com…
mesh/repo-check head ae9cda8531b3 merged into trunk 017b729bc95d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1792195559895153198 (novox/mesh-catalog at 82df3bd5 into main,…
mesh/delivery-group group feat/agents-write-git-ssh delivered: every member is delivered
2026-10-16 22:05:52 +00:00
jschoubben 6f45570d4c Do not remove keys or entries before the machines' keys are read (issue 955 F)
mesh/merge-gate head 6f45570d4c23 merged into trunk 017b729bc95d — fail: builds gitea, ssh-client → ace, g14, novox, shanks; no bus step; 4 problem(s) the …
mesh/repo-check head 6f45570d4c23 merged into trunk 017b729bc95d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery-group group feat/agents-write-git-ssh rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
2026-10-16 22:54:31 +02:00
jschoubben 6fdf3629d3 Remove a machine's mesh keys and entry when its key is no longer reported (issue 955 F) 2026-10-16 22:53:22 +02:00
jschoubben 896f1d2854 Merge feat/agents-write-git-ssh into feat/agents-write-git-ssh-f (issue 955 F) 2026-10-16 22:48:06 +02:00
jschoubben 5c64b71cd7 Test the forge-account provider whole against a real bus and a fake forge (issue 955 F5) 2026-10-16 22:47:59 +02:00
jschoubben 1081a5f3ea Declare the forge-account provision, its provider bundle, its fact and the keys it reads (issue 955 F4) 2026-10-16 22:46:27 +02:00
jschoubben c346bba34e Add the forge-account provider: an agent account, its team and its machine key per consumer (issue 955 F3) 2026-10-16 22:45:51 +02:00
fix-955-ssh 165cbcb35f Merge branch 'feat/agents-write-git-ssh' into feat/agents-write-git-ssh-s 2026-10-16 22:44:43 +02:00
fix-955-ssh 9cb615f121 Prove the forge key process over a real bus in the merge check (issue 955 S4) 2026-10-16 22:44:36 +02:00
fix-955-ssh ad5a0f8e44 Place the forge's known host, SSH Host and git rewrite (issue 955 S3) 2026-10-16 22:43:51 +02:00
fix-955-ssh a4cdd05cb2 Declare ssh-client's keys state and its forge-account requirement (issue 955 S2) 2026-10-16 22:43:29 +02:00
fix-955-ssh b2009f1bb5 Keep the machine's forge SSH key and its keys entry (issue 955 S1) 2026-10-16 22:43:18 +02:00
jschoubben 8131c13634 Register and prune an agent account's mesh SSH keys through the forge API (issue 955 F2) 2026-10-16 22:42:04 +02:00
jschoubben c8dfbdc813 Lift the agent account code into internal/forgeaccount for the forge-account provider (issue 955 F1) 2026-10-16 22:41:11 +02:00
mesh-tools 017b729bc9 Merge pull request 'Serve postgres's consumers the port its server is published on, not the container's 5432 (issue 957)' (#339) from fix/957-postgres-serves-published-port into main 2026-10-16 20:15:33 +00:00
lead-57 deb2020bd4 Serve postgres's consumers the port its server is published on, not the container's 5432 (issue 957)
mesh/merge-gate head deb2020bd47d merged into trunk 5652a1494493 — pass: builds postgres, sent nowhere; no bus step; 2 wait(s) for a person; every machine …
mesh/repo-check head deb2020bd47d merged into trunk 5652a1494493 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
On novox postgres is published 6852:5432; handed 5432, gitea and keycloak reached
nothing once their files were filled from postgres's entry.
2026-10-16 21:56:15 +02:00
mesh-tools 5652a14944 Merge pull request 'Feature agents-write-git, step 1: agents write with their own accounts, main stays closed, a merge into main waits for the operator (issues 948, 954)' (#338) from feat/agents-write-git into main 2026-10-16 19:45:36 +00:00
fix-deliv-awg@novox 9acd1f51c5 Merge branch 'feat/agents-write-git' into feat/agents-write-git-d
mesh/merge-gate head 9acd1f51c5cf merged into trunk 8d012fe4e270 — pass: builds gitea, mesh-delivery, mesh-provisioner-client → ace, g14, novox, shanks; no…
mesh/repo-check head 9acd1f51c5cf merged into trunk 8d012fe4e270 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-16 21:36:10 +02:00
fix-deliv-awg@novox 4cccd817a0 End a group's ask and Approve when any member is pushed (issue 948, ADR 0398 §3) 2026-10-16 21:36:06 +02:00
fix-forge-awg 3cdea45f90 Keep main's rule right at every start and copy a covering glob's checks (issue 948, review round 1) 2026-10-16 21:35:51 +02:00
lead-57 4215b40547 Read the whole plan the controller writes: at, from, as, names and credential-tool (issue 954)
The client decodes a plan strictly, and since the controller carries these fields
(issues 923 and 928) it refused the plans of gitea, keycloak and claude-code.
2026-10-16 21:32:13 +02:00
fix-deliv-awg@novox 0bfece71da Merge branch 'feat/agents-write-git' into feat/agents-write-git-d 2026-10-16 21:29:58 +02:00
fix-deliv-awg@novox f61e00aeaa Ask the operator to Approve a merge into main and merge only on the warrant (issue 948, ADR 0398 §3) 2026-10-16 21:29:55 +02:00
fix-forge-awg 789aa0133f Keep the guard's push wording for main (issue 948) 2026-10-16 21:28:49 +02:00
fix-forge-awg 7807135a1b Main takes no push and only mesh-tools merges, set by setup and guarded (issue 948, ADR 0398 §2) 2026-10-16 21:28:43 +02:00
fix-deliv-awg@novox 417f69567c Merge only into main: a ready mark on another base is left to its author (issue 948, ADR 0398 §4) 2026-10-16 21:26:53 +02:00
fix-forge-awg b0f86671b6 Give the agent's token write scopes and the agents write team (issue 948, ADR 0398 §1) 2026-10-16 21:25:59 +02:00
mesh-tools 8d012fe4e2 Merge pull request 'Feature providers-after-switch-on, step 1: providers answer after the switch-on (issues 923, 928, 849, 945, 825)' (#337) from feat/providers-after-switch-on into main 2026-10-16 19:13:01 +00:00
lead-57 d2992f81d1 Take mesh-sdk a4e7f48 in the modules that use its provider helper (issues 825, 849, 945)
mesh/merge-gate head d2992f81d17f merged into trunk 3817099b2ca2 beside mesh-controller#363@df5f01bf — pass: builds keycloak, mesh-provisioner-client, mesh…
mesh/repo-check head d2992f81d17f merged into trunk 3817099b2ca2 beside mesh-controller#363@df5f01bf — pass: its merge-check.sh passed, each part in its to…
mesh/delivery delivered
mesh/delivery-group group feat/providers-after-switch-on delivered: every member is delivered
postgres, keycloak, mesh-provisioner-client, route-proxy and route-adapter now
build against the feature's mesh-sdk: the harness package, the vault-name key
and the helper's fixes.
2026-10-16 20:59:14 +02:00
fix-cat-pas@novox dcedd8d2b2 Give the route plan entries their credential-form, and read the entry's vault-name in the route providers' tests (issues 923, 945) 2026-10-16 19:44:19 +02:00
fix-cat-pas@novox 302eab1431 Restore the test that both keycloak paths leave the same client, over the SDK's moved path (issue 849) 2026-10-16 19:37:54 +02:00
fix-cat-pas@novox b0dc3323c9 Run keycloak on the SDK's harness and moved path, dropping its copies (issue 849) 2026-10-16 19:36:02 +02:00
fix-cat-pas@novox 33759f8b57 Run postgres on the SDK's harness and moved path, dropping its copies (issue 849) 2026-10-16 19:35:22 +02:00
fix-vault-pas@novox 2f09dd8501 Merge branch 'feat/providers-after-switch-on' into feat/providers-after-switch-on-v 2026-10-16 19:34:15 +02:00
fix-vault-pas@novox 5a00596908 Make the vault ask a software-issuing provider's tool itself for holders that hold nothing (issue 928) 2026-10-16 19:34:11 +02:00
fix-cat-pas@novox a51e2c0fb4 Pin route-proxy's acme templates beside its manifest (issue 849) 2026-10-16 19:32:20 +02:00
fix-vault-pas@novox 044a548cee Pin that the client asks a refused or not-yet secret again (issue 928) 2026-10-16 19:31:40 +02:00
fix-vault-pas@novox 0dab4475cd Answer an unreadable assignment as not yet, never a refusal (issue 928) 2026-10-16 19:30:15 +02:00
fix-cat-pas@novox 0b056edaaa Prove the client fills gitea's origins and keycloak's hostname from route-proxy's entry for a controller-shaped plan entry (issue 923) 2026-10-16 19:29:30 +02:00
fix-cat-pas@novox cc9e7a72ee Name the vault's credential vault-name in the client's bound-entry fixtures (issue 945) 2026-10-16 19:29:30 +02:00
mesh-tools 3817099b2c Merge pull request 'Ask the plans again when the moved path starts before its runtime answers (issue 943)' (#336) from fix/943-moved-path-waits-for-runtime into main 2026-10-16 16:55:41 +00:00
mesh-tools 6005c6001e Ask the plans again when the moved path starts before its runtime answers (issue 943)
mesh/merge-gate head 6005c6001e2f merged into trunk 832a0afd1814 — pass: builds keycloak, postgres, sent nowhere; no bus step; 3 wait(s) for a person; ever…
mesh/repo-check head 6005c6001e2f merged into trunk 832a0afd1814 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
postgres and keycloak start their moved path beside Serve, and a watch
asked before Serve runs is refused by the runtime. Run returned on that
refusal and main only logged it, so a start that lost the race left the
provider answering nobody and retiring nothing until its next restart:
postgres on novox at 16:52, 18:29 and 18:48, on ace at 16:22, which kept
issue 937's retirement condition on novox from clearing. Run now asks
again, the wait doubling from a second to a minute, until the runtime
answers or its context ends.
2026-10-16 16:52:52 +00:00
mesh-tools 832a0afd18 Merge pull request 'Say once after a restart that nothing waits, so an old retirement condition clears (issue 937)' (#335) from fix/937-settle-after-restart into main 2026-10-16 16:45:30 +00:00
mesh-tools d7a880a608 Say once after a restart that nothing waits, so an old retirement condition clears (issue 937)
mesh/merge-gate head d7a880a60878 merged into trunk 3ee754eb3b0c — pass: builds keycloak, postgres, sent nowhere; no bus step; 3 wait(s) for a person; ever…
mesh/repo-check head d7a880a60878 merged into trunk 3ee754eb3b0c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The controller clears a provider's retire-waiting condition only on its
settled word, and a provider says settled only for a set its own memory
held. A provider restarted after the set stopped waiting (here: the
delivery of the grant-file fix) never says it, so the condition kept
asking the operator to approve retiring what nobody waits on. The first
pass of a process that finds every consumer asked now says settled once,
with no set; the controller clears both conditions on it.
2026-10-16 16:42:39 +00:00
mesh-tools 3ee754eb3b Merge pull request 'Keep serving the consumers postgres's grant file gives on its moved path (issue 937)' (#334) from fix/937-postgres-grants-on-moved-path into main 2026-10-16 16:32:21 +00:00
mesh-tools 6268f8f94b Merge pull request 'Hold an ask no channel can carry now and offer it when one can (issue 926, ADR 0397)' (#333) from fix/926-a-held-ask-waits-for-a-channel into main 2026-10-16 16:28:21 +00:00
mesh-tools 41cdd03208 Keep serving the consumers postgres's grant file gives on its moved path (issue 937)
mesh/merge-gate head 41cdd032084e merged into trunk 6268f8f94bda — pass: builds keycloak, postgres, sent nowhere; no bus step; 3 wait(s) for a person; ever…
mesh/repo-check head 41cdd032084e merged into trunk 6268f8f94bda — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Only a moved consumer's plan names postgres, so on the moved path every
consumer still given by the controller's grant file was counted unasked
and offered for retirement (ace 5 of 5, novox 7 of 9), and could have
been retired unasked where the bound allowed. The moved path now reads
MESH_RECEIVES again: its logins are asked, the ones no plan names are
made and checked by the harness's grant pass until R1 (issue 682), and
an unreadable grant file retires nothing (issue 241). The harness's
grant pass becomes apply, identically in keycloak's copy.
2026-10-16 16:25:52 +00:00
jschoubben 77d751fe83 Test the router whole over the real bus: a held ask is offered when a channel can carry it (issue 926, ADR 0397)
mesh/merge-gate head 77d751fe830d merged into trunk 08188663faff — pass: builds mesh-knowledge, messenger → novox; no bus step; every machine composes with…
mesh/repo-check head 77d751fe830d merged into trunk 08188663faff — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-16 17:57:52 +02:00
jschoubben c211762813 Hold an ask no channel can carry now and offer it when one can; show held and why (issue 926, ADR 0397)
The router refused such an ask with 'no channel', so the asker waited for an Approve nobody saw. It is now held as an open ask with the per-kind reason, judged again when the kinds or a channel's standing change and every minute, and the knowledge base says held and why.
2026-10-16 17:51:32 +02:00
mesh-tools 08188663fa Merge pull request 'mesh-files holds node-files: file work on a node with trash, owned paths and an act log, no shell needed (issue 900, ADR 0394)' (#332) from feat/900-node-files into main 2026-10-16 14:25:12 +00:00
jschoubben d4a9260847 Merge remote-tracking branch 'origin/main' into feat/900-node-files
mesh/merge-gate head d4a9260847ec merged into trunk 8b53a3663244 beside mesh-controller#360@f4ddddd5 — pass: builds new: modules/mesh-files, sent nowhere; …
mesh/repo-check head d4a9260847ec merged into trunk 8b53a3663244 beside mesh-controller#360@f4ddddd5 — pass: its merge-check.sh passed, each part in its to…
mesh/delivery delivered
mesh/delivery-group group feat/900-node-files delivered: every member is delivered
2026-10-16 16:15:45 +02:00
jschoubben 8b53a36632 Merge pull request 'mesh-delivery: mark-ready writes label then comment in the order the forge counts; a label awaiting its comment is not cleared (issue 913, issue 873)' (#331) from feat/913-mark-ready into main
Reviewed-on: #331
2026-10-16 13:47:38 +00:00
jschoubben 5d98bcc0db mesh-files: the holder of node-files, reads and writes with trash, owned paths and an act log, so file work on a node needs no shell (issue 900, ADR 0394)
mesh/delivery-group group feat/900-node-files checking: composed check out of date: novox/mesh-controller@210f7cf5e64b was judged against trunk 611e2275cf7c, t…
mesh/merge-gate out of date: judged against trunk 3db85a0dde83, the trunk is now 8b53a3663244; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 3db85a0dde83, the trunk is now 8b53a3663244; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
2026-10-16 14:50:03 +02:00
jschoubben 2d1b20364f mesh-delivery: an empty-head ready mark is cleared only after a minute, and not right after mark-ready; clearer wording (issue 913, issue 873)
mesh/merge-gate head 2d1b20364f9b merged into trunk 3db85a0dde83 beside mesh-controller#357@114c99e0 — pass: builds mesh-delivery → novox; no bus step; eve…
mesh/repo-check head 2d1b20364f9b merged into trunk 3db85a0dde83 beside mesh-controller#357@114c99e0 — pass: its merge-check.sh passed, each part in its to…
mesh/delivery delivered
mesh/delivery-group group feat/913-mark-ready delivered: every member is delivered
2026-10-16 14:35:58 +02:00
jschoubben 741b729ad4 mesh-delivery: mark-ready labels then comments the head in the order the forge counts; an empty-head clear says why (issue 913, issue 873) 2026-10-16 14:33:50 +02:00
mesh-tools 3db85a0dde Merge pull request 'Feature 57 (config and provisioning): release the catalogue' (#330) from release/57-config-and-provisioning into main 2026-10-16 10:57:56 +00:00
mesh-tools a31ad48b02 Restore the blank last line the patch route dropped from ten files, so the branch holds the reviewed tree (issue 875)
mesh/merge-gate head a31ad48b024d merged into trunk ee1b15d7f066 — pass, with a note: builds adwaita, artifact-store-tools, asus-zephyrus-g14, baserow, bet…
mesh/repo-check head a31ad48b024d merged into trunk ee1b15d7f066 — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery delivered
2026-10-16 09:43:59 +00:00
mesh-tools 0260ae92f6 Feature 57 (config and provisioning): release the catalogue, the reviewed release branch merged with main, on top of main 2026-10-16 09:42:45 +00:00
mesh-tools ee1b15d7f0 Merge pull request 'A delivering group says success on mesh/delivery-group, so its later members can merge (issue 902)' (#329) from fix/902-group-member-merges-while-delivering into main 2026-10-16 09:16:36 +00:00
mesh-tools 8efe499db9 A delivering group says success on mesh/delivery-group, so its later members can merge (issue 902)
mesh/merge-gate head 8efe499db965 merged into trunk 173c5d5ef434 — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head 8efe499db965 merged into trunk 173c5d5ef434 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-16 09:12:53 +00:00
mesh-tools 173c5d5ef4 Merge pull request 'Put a group's composed check out of date when a member is rechecked (issue 867, ADR 0323 §6)' (#327) from fix/867-recheck-asks-the-group-again into main 2026-10-16 01:20:20 +00:00
mesh-toolsandClaude c7c784161d Put a group's composed check out of date when a member is rechecked (issue 867, ADR 0323 §6)
mesh/merge-gate head c7c784161dcd merged into trunk 0fdacb98399d — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head c7c784161dcd merged into trunk 0fdacb98399d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
After mesh-host#114's recheck passed, group 57-config-and-provisioning kept its failed composed verdict for the same heads and was never asked again. A recheck now marks the member's group's composed check out of date, answered or still running, so it is asked again once every member is judged again and ready; a late answer from the old run is dropped.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-16 01:17:58 +00:00
mesh-tools 0fdacb9839 Merge pull request 'mesh-mail: a send ask the operator channel can carry (issue 868)' (#326) from fix/868-a-send-ask-the-channel-can-carry into main 2026-10-16 00:08:21 +00:00
jochen b0e4a12c88 mail_send's ask: judge messenger's real words, carry the content rule at run time, match display-name recipients (issue 868, review round 1)
mesh/merge-gate head b0e4a12c8840 merged into trunk 21a499ba5976 — pass: builds mesh-mail → novox; no bus step; every machine composes with the change as i…
mesh/repo-check head b0e4a12c8840 merged into trunk 21a499ba5976 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Review round 1: the test judged Headline+Explanation+Details, but messenger refuses an ask as a whole on
Headline, Explanation, OnExpiry and each option's Label and Does alone (asks.go's TakeAsk); Details is
filtered line by line instead (holder.go's sayOn). send.go's Ask() now builds that exact judged text and
checks it with messenger's real content rule, carried at run time as content_rule.go (messenger's
content.go, renamed, byte-identical, pinned by copies_test.go) rather than only in a test copy. When a
subject or first line the masking does not catch (an IP, a hardware address, a path-shaped reference, a
hex hash, ...) would still refuse the whole ask, the Explanation drops the subject and first lines and
points at Details instead, so the ask always reaches the operator. A recipient written with a display
name ('Name <addr>') is now matched against the operator's own accounts, and masked, by its address alone
(net/mail.ParseAddress).
2026-10-16 02:04:32 +02:00
jochen a76b0742ff mail_send's ask names recipients and masks addresses so the operator channel can carry it (issue 868)
messenger's content rule (ADR 0253 §3) refuses any message carrying a mail address, so mail_send's ask
never reached the operator: no send could ever be approved. A recipient that is one of the operator's own
mesh-mail accounts is now named by that account's name; any other loses its address and host; the subject
and body's first lines have every address and link masked before they reach the ask. The mail itself is
sent unchanged.

Tests run messenger's real content rule against the asks mail_send builds (content.go copied verbatim as
content_test.go, pinned byte-identical by copies_test.go, since messenger's package main cannot be
imported from another module).
2026-10-16 01:57:06 +02:00
mesh-tools 21a499ba59 Merge pull request 'Group siblings: mesh-delivery names member heads; the forge re-pends and refuses on a sibling's move (issue 867, ADR 0382)' (#325) from fix/867-group-siblings into main 2026-10-15 23:53:10 +00:00
mesh-toolsandClaude de9d29dc65 Keep a group member's siblings and refuse its merge until the group passed and every sibling is reviewed (issue 867, ADR 0382)
mesh/merge-gate head de9d29dc657d merged into trunk 62ec68355cd1 — pass: builds gitea, mesh-delivery → novox; no bus step; every machine composes with the …
mesh/repo-check head de9d29dc657d merged into trunk 62ec68355cd1 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/delivery-group group fix/867-group-siblings delivered: every member is delivered
mesh-delivery's groups names each member's pull request, head, base and whether it is open (member_details), so the controller can read a member's siblings beside its check. The forge's holder keeps the siblings each verdict read, puts that verdict out of date when a sibling gets a new head, closes, or its base moves by anything but its own merge, and the merge tool merges such a member only when mesh/delivery-group passed at the heads read and each sibling head is approved. The statuses name the siblings, or say they could not be learned.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-15 23:27:13 +00:00
mesh-tools 62ec68355c Merge pull request 'mesh-mail-notifier asks its own machine's node-notifier (issue 865)' (#324) from fix/865-notifier-asks-its-own-machine into main 2026-10-15 21:34:27 +00:00
mesh-tools 6a5e6510ca Merge pull request 'The clipboard service waits for an older daemon's lock, and the history sees the daemon (hq issues 860, 862)' (#323) from fix/860-862-the-clipboard-takes-over-quietly into main 2026-10-15 21:31:46 +00:00
jochen 3f97d2d50d mesh-mail-notifier: Send names its own machine's node-notifier (issue 865)
mesh/merge-gate head 3f97d2d50d57 merged into trunk 6a5e6510cab2 — pass: builds mesh-mail-notifier → g14, shanks; no bus step; every machine composes with …
mesh/repo-check head 3f97d2d50d57 merged into trunk 6a5e6510cab2 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-15 23:27:24 +02:00
jschoubben 6112233649 Probe clipmenud's lock with flock's default conflict status, which BusyBox shares (issue 862)
mesh/merge-gate head 6112233649d3 merged into trunk 8c2d7e5f97f1 — pass: builds clipmenu → g14, shanks; no bus step; every machine composes with the change…
mesh/repo-check head 6112233649d3 merged into trunk 8c2d7e5f97f1 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-15 23:27:19 +02:00
mesh-tools 8c2d7e5f97 Merge pull request 'mesh-mail-notifier: the watcher runs in its runtime, not a process that exits at once (issue 859)' (#322) from fix/859-notifier-runs-in-its-runtime into main 2026-10-15 21:21:12 +00:00
jschoubben 21ebbffed4 Count clipmenud when it runs as a bash script (issue 860)
mesh/merge-gate head 21ebbffed469 merged into trunk 8c2d7e5f97f1 — pass: builds clipmenu → g14, shanks; no bus step; every machine composes with the change…
mesh/repo-check head 21ebbffed469 merged into trunk 8c2d7e5f97f1 — fail: its merge-check.sh failed: --- FAIL: TestTheLauncherWaitsQuietlyWhileAnotherClipme…
mesh/delivery superseded: a newer head of the same pull request
2026-10-15 23:18:22 +02:00
jochen 31120ed580 mesh-mail-notifier: watch retries a subscribe refusal instead of exiting (issue 859, review round 1)
mesh/merge-gate head 31120ed58029 merged into trunk c9848f4fb85c — pass: builds mesh-mail-notifier → g14, shanks; no bus step; every machine composes with …
mesh/repo-check head 31120ed58029 merged into trunk c9848f4fb85c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
A subscribe the runtime refuses no longer calls os.Exit(1), which took mail-notifier.status and
mail_notifier_health down with it: watch now retries with backoff (2s doubling to 1 minute), logging each
refusal, as mesh-knowledge's hear does. Also: the run() test proves liveness by having the watcher answer a
ping after run returns, instead of checking a channel only the test's own deferred cleanup could close; and
the manifest test is renamed to say what it checks (no process resource at all).
2026-10-15 23:17:14 +02:00
jschoubben cf304d8629 Wait quietly for clipmenud's session lock; count the operator's clipmenud (issues 860, 862) 2026-10-15 23:15:35 +02:00
jochen 12d7852082 mesh-mail-notifier: the watcher runs in the base invocation, not a separate notify process (issue 859)
The notify process's stdin is empty under systemd, so stdio.Serve returned at EOF, main returned, and
systemd restarted it every five seconds: the watch loop never ran long enough to show a notice. The watch
loop now starts beside stdio.Serve in the one process the node's runtime launches and keeps alive, as
mesh-knowledge's serve does. No resource declares a separate notify process any more.
2026-10-15 23:13:34 +02:00
mesh-tools c9848f4fb8 Merge pull request 'The clipboard keeps its history running and keeps it to itself (hq issues 743, 750)' (#320) from fix/743-750-the-clipboard-keeps-history-and-keeps-it-to-itself into main 2026-10-15 21:02:04 +00:00
jschoubben c070bea782 Declare how the clipboard daemon is ready, so the count of resources without health stays at 62 (ADR 0240 rule 8, issues 743, 750)
mesh/merge-gate head c070bea782aa merged into trunk 7443938fef69 — pass: builds clipmenu → g14, shanks; no bus step; every machine composes with the change…
mesh/repo-check head c070bea782aa merged into trunk 7443938fef69 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-15 22:59:27 +02:00
mesh-tools 7443938fef Merge pull request 'gitea_commit takes a large patch from a hash-checked spool file (issue 852)' (#319) from fix/852-commit-a-patch-file into main 2026-10-15 20:55:15 +00:00
mesh-tools aee97bf8ad Merge pull request 'The kernel module's transparent-huge-pages defaults to madvise (hq issue 856, ADR 0381)' (#321) from fix/856-the-kernel-module-runs-everywhere into main 2026-10-15 20:47:50 +00:00
jschoubben c816cafc00 The kernel module's transparent-huge-pages defaults to madvise, the mesh's chosen value (hq issue 856, ADR 0381 §2, §3)
mesh/merge-gate head c816cafc00bd merged into trunk de6657c846b9 — pass: builds kernel → ace, g14, novox, shanks; no bus step; every machine composes with …
mesh/repo-check head c816cafc00bd merged into trunk de6657c846b9 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-15 22:45:07 +02:00
jschoubben 2353350d79 The clipboard keeps its history running and keeps it to itself (hq issues 743, 750)
mesh/merge-gate out of date: judged against trunk aee97bf8ad42, the trunk is now 7443938fef69; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk aee97bf8ad42, the trunk is now 7443938fef69; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
clipmenud is declared as the operator's user unit, enabled and running, with a launcher that finds the display. Copy reads the clipboard back and fails with the reason. History and paste answer counts, times and lengths, never content.
2026-10-15 22:43:42 +02:00
mesh-toolsandClaude d4b8b24701 Let gitea_commit take a large patch from a hash-checked spool file (issue 852)
mesh/merge-gate head d4b8b2470159 merged into trunk aee97bf8ad42 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head d4b8b2470159 merged into trunk aee97bf8ad42 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
A feature's diff is megabytes, too large to type into a call. gitea_commit now also takes patch_file, the
random hex name of a file in the module's spool directory /var/lib/mesh/forge-patches (mode 0733, owned by
the machine's account), with patch_sha256. The tool reads only that directory, never through a symlink or a
hard link, refuses a file whose SHA-256 differs, applies it as it applies patch, and removes it.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-15 20:39:13 +00:00
mesh-tools de6657c846 Merge pull request 'An active research effort takes new versions, reviewed as proposals (hq issue 844)' (#318) from fix/844-an-active-effort-takes-new-versions into main 2026-10-15 19:35:07 +00:00
jschoubben f75e1a91d9 Carry the proposal day only into the dated rules, so an old effort's marked insight stands (hq issue 844)
mesh/merge-gate head f75e1a91d957 merged into trunk a95b3fa053e0 — pass: builds mesh-knowledge → novox; no bus step; every machine composes with the change…
mesh/repo-check head f75e1a91d957 merged into trunk a95b3fa053e0 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-15 21:29:55 +02:00
jschoubben ebfec39937 Hold an active effort's new version to the dated rules of the day it is proposed (hq issue 844) 2026-10-15 21:26:55 +02:00
jschoubben 24d05701d3 An active research effort takes new versions, reviewed as proposals (hq issue 844) 2026-10-15 21:22:17 +02:00
mesh-tools a95b3fa053 Merge pull request 'Mail seats: mesh-mail declares them, mesh-mail-notifier shows new mail, messenger tells (issues 800, 797)' (#317) from feat/mail-seats into main 2026-10-15 18:04:19 +00:00
jochen c2cd2e1eb1 issue 800: fix review round 3 finding, a true sliding window instead of a session quota
mesh/merge-gate head c2cd2e1eb15e merged into trunk 789b0a9474c8 — pass: builds mesh-mail, messenger, new: modules/mesh-mail-notifier → novox; no bus step;…
mesh/repo-check head c2cd2e1eb15e merged into trunk 789b0a9474c8 — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery delivered
mesh/delivery-group group feat/mail-seats delivered: every member is delivered
A session that only reset after a quiet minute kept counting, and so folding, a steady
trickle of mail forever even with no burst (one mail every 50s). N is now the account's
qualifying mail arrived in the trailing 60 seconds, counted fresh on every arrival; a fold
holds only its own overflow and the notice names just that count, not the whole history.
2026-10-15 19:03:05 +02:00
jochen aa62de2e78 issue 797: a dropped tell no longer crowds the conditions' cap out of its own history
A dropped tell was kept in the same bounded send history (KeptSends)
that the conditions' and notify's cap counts sends from. A flood of
drops could push real, still-recent sends out of that bounded history,
undercounting sentLastHour and letting the conditions' cap stop
holding. A dropped tell is still said once an hour in the log and
named in Tell's own "dropped" answer; it is simply never recorded into
that shared history any more.
2026-10-15 18:51:26 +02:00
jochen 66713c5a1e issue 800: fix review round 2 findings in mesh-mail-notifier
A buffer was replaced, discarding whatever it still held unsent, once
Window had passed since its first mail — losing a failed send waiting
for its retry, or a folded mail waiting for its minute's end, if a new
mail arrived meanwhile. The window now slides on the gap since the
last mail, not on time elapsed since the first: a session's count
never resets, and its buffer is never replaced, while anything in it
is still unsent.

The fold notice now counts every mail of the account in that minute,
not only the overflow, and says "mail" for one and "mails" for more.
2026-10-15 18:50:21 +02:00
jochen b771794f1c issue 800: keep notify's health as unit; the gate refuses a one-process module judged only by its own tool
ADR 0227 rule 8 / ADR 0240 rule 2 refuse a module with no health check of
another kind beside a tool check. mesh-mail-notifier has one process
(notify); wiring mail_notifier_health as its manifest health, as the review
note's wording suggested (mirroring mesh-mail's watch/mail_health), trips
that rule because mesh-mail also has keep (kind unit) beside watch.
mail_notifier_health stays built, tested and callable; notify's manifest
health stays kind unit until the lead decides how a one-process module
gets this automatically.
2026-10-15 18:33:22 +02:00
jochen 6c63a17cfa Merge branch 'feat/mail-seats' into ms-800-r1 2026-10-15 17:54:33 +02:00
jochen ed66a2e873 issue 800: fix review round 1 findings in mesh-mail-notifier
Flush now sorts accounts instead of walking a map, so notice order is
stable. A failed node-notifier.send keeps its mail pending and retries
at most once a minute, without holding up other accounts' notices, and
the failure is kept in the Store for a new health tool
(mail_notifier_health) that is unhealthy past ten minutes of failure.
The burst rule now follows the lead's reading of ADR 0378 S9: within an
account's sliding minute, the first three qualifying mails are shown
one by one and the rest are folded into one notice at the window's
end; there is no separate quiet-gap timer. The dedupe key is the
account and message id, falling back to folder/uidvalidity/uid only
when no message id is given, so a UIDVALIDITY reset cannot suppress a
new mail, and a redelivered event already buffered is never recounted.
status now lists every granted account, not only those with a notice,
and caches mail-store.accounts for a minute.
2026-10-15 17:54:27 +02:00
jochen cc73b38671 issue 797: tells no longer eat the conditions'/notify's cap, and a dropped tell is logged once an hour
Review round 1 of mesh-catalog found sentLastHour counting tells
against CapPerHour (ADR 0380 §8 says tells keep their own cap, never
shared), and the drop log firing on every drop instead of once an
hour per channel as the comment already said.
2026-10-15 17:46:39 +02:00
jochen 0993c31ad4 Merge branch 'feat/mail-seats' into ms-797 2026-10-15 17:37:22 +02:00
jochen 82475691b2 issue 797: messenger gains the tell verb for a plain notice
ADR 0380 §8: the operator channel's notify is shaped for conditions (a key that
stays open, escalation, a plain rule that rewrites a subject), so a module has
no way to say a plain notice such as new mail. tell sends a headline and one or
two lines at once to every channel, bypassing routing, the burst window and the
walk hold; it offers Details like any message, is kept in the history, but its
record is marked cleared at once so it never stays open. Its cap (10 a channel
an hour) is its own, apart from notify's and the conditions'.
2026-10-15 17:37:15 +02:00
jochen 89046e6613 mesh-mail's mail-notifier needs a list of node seats, as the controller reads it (issue 800) 2026-10-15 17:34:00 +02:00
jochen dc89995d96 Add mesh-mail-notifier, the default holder of mail-notifier (issue 800 part B)
ADR 0378 §9 as amended by ADR 0379: a module of its own, built from mesh-mail's
event shape, runs as its own account to hear mesh-mail's restricted MAIL stream
(ADR 0376 §2), shows each new inbox mail through node-notifier.send at most
once, folds more than three from one account into one notice, and emits its
own shown event.
2026-10-15 17:33:15 +02:00
jochen 6c10288f0b mesh-mail declares the mail seats and claims mail-store (issue 800 part A)
Declares mail-store and mail-webclient (held once for the mesh) and mail-notifier
(held once per machine, needing node-notifier) per ADR 0378 as amended by ADR 0379.
Claims mail-store and serves its four verbs through the same handlers as the
existing mail_* tools. Moves the event shape (EventKey, Changes) into its own
file, mailevent.go, so mesh-mail-notifier (issue 800 part B) can carry a
byte-identical copy. Adds checks/mailstore, refusing any manifest but mesh-mail's
own that claims mail-store (ADR 0378 §3), wired into merge-check.sh.
2026-10-15 17:16:53 +02:00
mesh-tools 789b0a9474 Merge pull request 'Mail and bus hardening: mesh-mail sends on Approve, desk prompt says what for, duplicate PR delivery, cpu_top (issues 790, 789, 783, 741, 811)' (#316) from feat/mail-hardening into main 2026-10-15 14:59:25 +00:00
jochen eb97a3d9a2 mesh-mail: refuse a send whose undecided index cannot be written, and never overwrite a corrupt index (issue 811, review round 3)
mesh/merge-gate head eb97a3d9a2d7 merged into trunk 0914a33fba5a — pass: builds disk-load, mesh-delivery, mesh-mail, rofi → ace, g14, novox, shanks; no bus…
mesh/repo-check head eb97a3d9a2d7 merged into trunk 0914a33fba5a — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/delivery-group group feat/mail-hardening delivered: every member is delivered
2026-10-15 15:40:16 +02:00
jochen 8b2e2300d8 mesh-mail: a send whose ask was never heard settles as ended, and the sweep reads only undecided sends (issue 811)
A send whose ask never reached the router (the publish failed, or it was otherwise never recorded) stayed kept and
undecided forever, so mail_send_status kept saying "waiting" and the minute sweep kept reading it. Settle it as
ended once its asked time plus SendAskLasts has passed, with the words that say why. Keep an index of undecided
send ids beside the sends (written when kept, removed once decided) so the sweep's work grows with undecided
sends only, not with every send ever kept.
2026-10-15 15:27:56 +02:00
jochen 2f2daea415 Merge branch 'feat/mail-hardening' into mh-790-r1 2026-10-15 15:08:21 +02:00
jochen 3b48f5962a mesh-mail: send on the operator's Approve itself, sweep, stale-send status, no header injection (issue 790, review round 1) 2026-10-15 15:07:46 +02:00
jochen 2b1678f315 disk-load: cpu_top's shares use the unrounded busy time, and key units by cgroup path (issue 741) 2026-10-15 15:06:06 +02:00
jochen ab739d02dd mesh-delivery: a merge that starts over a stopped duplicate is made from the forge's word too (issue 783) 2026-10-15 15:01:23 +02:00
jochen a60020b51a issue 741: merge feature branch head 2026-10-15 14:51:12 +02:00
jochen a8d84d1730 Add disk-load's cpu_top tool: CPU by systemd unit and container (issue 741)
No tool named which units or processes used CPU outside containers during
the novox overload (issue 739). cpu_top reads each cgroup's cpu.stat (no
root needed), ranks units or containers busiest first, and says the
machine's CPU in use and the busy time no unit accounts for.
2026-10-15 14:51:08 +02:00
jochen 9e8521afb5 mesh-delivery: a stopped duplicate's delivery follows the pull request that merges it (issue 783)
A delivery keyed by commit stayed stopped forever once an earlier, closed
duplicate pull request with the same head had stopped it, even though a
later pull request went on to merge that very commit: the merge only set
merged_as on the stopped record, and the walk that carried it waited for
this owner's word with no delivery left to give it. PullUpdated and
PullMerged now start such a delivery over, under the same id, for the
pull request that announces or merges it next, naming both pull requests
in the transition's why. Any other stop (a person's, a group's, the same
pull request's own close) is untouched.
2026-10-15 14:50:57 +02:00
jochen 8c60604eb7 Show what a desk's secret prompt is for, and declare mesh-mail's (issue 789)
The desk's prompt named only the module and the secret's key, so the operator
asking for one of mesh-mail's mail-credential-* accounts could not tell which
one. node-launcher.secret (modules/rofi) now shows the controller's optional
what as its own line under the module and secret names, escaped as text so
the catalogue's words never reach the prompt as markup; omitted as before
when none is declared. mesh-mail's mail-credential-* family now declares that
line with the long-form own-secrets shape the controller's manifest parser
reads (novox/hq issue 789 part b; part a landed in mesh-controller).
2026-10-15 14:28:45 +02:00
jochen 13cd710071 mesh-mail: add mail_send, asked and sent once on the operator's Approve (issue 790) 2026-10-15 13:57:47 +02:00
mesh-tools 0914a33fba Merge pull request 'mesh-mail: byte-bounded fetch, accounts only to granted callers, grants error kept, secret only stat'ed (hq issue 733)' (#315) from adr-359-mesh-mail-733 into main 2026-10-15 08:49:32 +00:00
jochen b5417c28d0 Merge remote-tracking branch 'origin/main' into adr-359-mesh-mail-733
mesh/merge-gate head b5417c28d026 merged into trunk 5f6a41146c3a — pass: builds mesh-mail, sent nowhere; no bus step; every machine composes with the chang…
mesh/repo-check head b5417c28d026 merged into trunk 5f6a41146c3a — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery covered after its stop: a later walk that contains it was delivered: plan-1792054175111921309 (novox/mesh-catalog at 0914a33f into main, do…
2026-10-15 10:44:37 +02:00
mesh-tools 5f6a41146c Merge pull request 'mesh-mail: the operator's mail accounts, watched and said on the bus — stage 1, own server read-only (hq ADR 0359)' (#300) from adr-359-mesh-mail into main 2026-10-15 08:44:07 +00:00
mesh-tools c71c513027 Merge pull request 'Read branch protection as the forge's administrator, so builds register again (issue 772, ADR 0375)' (#313) from 772-protection-read-as-admin into main 2026-10-15 00:22:49 +00:00
mesh-toolsandClaude 2e32f47e45 Read branch protection as the forge's administrator, so builds register again
mesh/merge-gate head 2e32f47e4596 merged into trunk 9492278d934b — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 2e32f47e4596 merged into trunk 9492278d934b — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1792097717281845096 (novox/mesh-catalog at 7443938f into main,…
The forge answers a repository's protection rules to its administrator
alone, and the tools' account no longer is one (ADR 0365), so every
delivery building a module failed at the controller's build-source check
with a 403. gitea_branch_protection_get now reads the rules with the
administrator account (ADR 0375); it only reads, and the protection set
and every write stay refused to the tools' account. Issue 772.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-15 00:18:39 +00:00
mesh-admin 9492278d93 Merge pull request 'Make the forge's tools act as an account of their own, not the administrator' (#311) from 365-forge-identities into main 2026-10-14 22:51:36 +00:00
mesh-admin 5ce2d07c7f Merge pull request 'Give a rollout the word of a stopped delivery that waits for it (hq issue 753)' (#312) from fix/753-a-rollout-is-never-left-without-a-word into main 2026-10-14 22:35:38 +00:00
jschoubben 373f339750 Give a walk the word of a stopped delivery that waits for it (hq issue 753)
mesh/merge-gate head 373f33975077 merged into trunk 7913b56d66f7 — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head 373f33975077 merged into trunk 7913b56d66f7 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
A delivery its group stopped for a member delivered since waits for a later
walk of its repository that contains its merge (issue 747). When that walk
carried no living delivery, nothing gave it its word and it waited forever.
The delivery now takes its turn on the walk that answers its merge, in its
group's order. A walk owed such a word stays under the orphans' timer, so
past the grace without it `stalled` lists it with why (held, or the
controller's refusal), and a walk no living delivery holds names the ended
deliveries it carries.
2026-10-15 00:30:26 +02:00
mesh-adminandClaude Opus 5.5 48f0662fde Make the forge's tools act as an account of their own, not the administrator (issue 469, ADR 0365 clause 2)
mesh/merge-gate head 48f0662fdea2 merged into trunk 5ce2d07c7f47 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 48f0662fdea2 merged into trunk 5ce2d07c7f47 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The tools' token reached everything the administrator may do, so their boundary rested on their own guards.
The gitea module now makes a plain account, mesh-tools, with the admin login, gives it write on code, issues
and pull requests of every novox repository through one team, and mints the tools' token for it; the admin's
old token goes only after the new one is kept. The Go forge bundle sends that token, never the admin login.
The forge shows protection rules only to a repository administrator, so every check reads a branch's
protection from the branch. mesh-admin stays a ready author, so marks made before the move keep counting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:28:25 +00:00
mesh-admin 7913b56d66 Merge pull request 'Add the kernel module: transparent huge pages set per machine (hq issue 746, ADR 0369)' (#310) from fix/746-huge-pages-madvise into main 2026-10-14 21:52:49 +00:00
jschoubben ab1811d003 Reload, not restart, the huge pages unit when the setting changes (hq issue 746)
mesh/merge-gate head ab1811d00357 merged into trunk c79337579d6b — pass: builds new: modules/kernel, sent nowhere; no bus step; every machine composes with…
mesh/repo-check head ab1811d00357 merged into trunk c79337579d6b — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
A restart ran restore then apply, so every change passed back through the
found mode and rotated the journal again. Reload applies the new value
directly; restore also drops its state; no rotation while the machine boots;
the unit is sandboxed.
2026-10-14 23:42:49 +02:00
mesh-admin c79337579d Merge pull request 'Make the forge's webhook a system one, and say whether its events are heard (hq issue 729)' (#309) from fix/729-the-forge-events-are-heard-or-said into main 2026-10-14 21:40:44 +00:00
jschoubben 3a84044ece Add the kernel module: transparent huge pages set per machine (hq issue 746)
With THP 'always' journald's mmapped journal files sit in 2 MiB folios, so each small
journal write dirties and writes back 2 MiB; on the control node that saturated the
root mirror holding the bus store and the store, and the controller lost its lease.
The module sets the mode from a per-machine setting (default always, the kernel's own,
so assigning it changes nothing until a machine sets madvise), verifies it took, rotates
the journal once so new files are mapped under the new mode, gives back what it found
when stopped, and serves kernel_huge_pages to measure file_thp per unit.
2026-10-14 23:31:13 +02:00
mesh-admin c51357f8af Merge pull request 'Cover a failed or stopped delivery once a later finished walk contains it (hq issue 747)' (#308) from fix/747-a-failed-member-is-covered-by-a-later-walk into main 2026-10-14 21:30:10 +00:00
jschoubben 2833a049aa Make the forge's webhook a system one, and say whether its events are heard (hq issue 729)
mesh/merge-gate head 2833a049aa0f merged into trunk c51357f8afd4 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 2833a049aa0f merged into trunk c51357f8afd4 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
POST /admin/hooks on gitea 1.27.3 makes a default webhook unless
config.is_system_webhook is "true": a default webhook fires for no existing
repository, and the system listing does not name it, so every start made
another and no event reached the receiver. Its edit never changes a secret
or a kind, so the new system webhook is made first and read back, then every
other hook at the receiver is deleted: system, default, and each
repository's copy of a default one, which would send its events twice.

gitea_events says where the receiver listens, the hook as ensured and what
it replaced, the verified and refused deliveries and the polls' pace. When
nothing verified for ten minutes, since the start or the last delivery,
while the poll found two heads or merges no event said, it is logged loudly
once per silent spell and answered unhealthy; so are deliveries refused
since the last verified one.
2026-10-14 23:28:41 +02:00
jschoubben 6f7f07635e Cover a failed or stopped delivery once a later finished walk contains it (hq issue 747)
mesh/merge-gate head 6f7f07635e02 merged into trunk 0f76b6e232a5 — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head 6f7f07635e02 merged into trunk 0f76b6e232a5 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
A group member whose walk failed in an outage stayed failed after a later walk
of its repository, containing its merge, was done; its group then stopped every
later walk of the other repository, and the catalogue could deliver nothing.

A failed or stopped delivery now reads covered when a done walk of its
repository, made after its merge, whose commit contains it (the forge's word,
as issue 362's containment), built every module it moves. A group counts it as
delivered, and a member the group stopped for it no longer holds a walk back:
the group stops a walk only for a member whose change is still undelivered.
A stop covered under issue 480 now likewise makes its group read reached, not
stopped.

While the forge is asked, the walk is held back rather than stopped, for at
most ten minutes from the first ask: a forge that keeps erroring or cannot say
then lets the walk be asked to leave the stopped merge out, as before. The
hold is said by waits, stalled and stalled walks. The pass reads kept walks
each tick, so today's stuck deliveries clear after a restart.
2026-10-14 23:19:06 +02:00
mesh-admin 0f76b6e232 Merge pull request 'Let an agent's session run the TypeScript check outside the toolchain image (issue 742)' (#307) from 742-ts-toolchain into main 2026-10-14 21:06:32 +00:00
mesh-adminandClaude Opus 5.5 3e16c055be Let an agent's session run the TypeScript check outside the toolchain image (issue 742)
mesh/merge-gate head 3e16c055be8e merged into trunk 0361c44a03a0 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 3e16c055be8e merged into trunk 0361c44a03a0 — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check-typescript.sh): main's version judged…
mesh/delivery delivered
Outside the image, assemble the same node_modules in the check's own
workspace from mesh-tools' lock on the forge, so a module's TypeScript is
checked locally before a push.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:03:53 +00:00
mesh-admin 0361c44a03 Merge pull request 'Clear a forge token name held twice, and mint one process at a time (hq issue 740)' (#306) from fix/a-token-name-held-twice-is-cleared into main 2026-10-14 21:03:29 +00:00
jschoubben d1debdcce8 Clear a forge token name held twice, and mint one process at a time (hq issue 740)
mesh/merge-gate head d1debdcce816 merged into trunk 0e7d9e74d22a — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head d1debdcce816 merged into trunk 0e7d9e74d22a — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The runtime starts the watcher and the tools as two processes over one kept
token. Both minted at once on 2026-10-14 and left two tokens named
mesh-tools; gitea refuses a delete by a name held twice, so every mint
failed and every forge tool with it. A replace now deletes each token by
the name by its id, from the account's whole list, and a mint holds a lock
file beside the kept token and reuses what another process minted.
2026-10-14 22:38:38 +02:00
mesh-admin 0e7d9e74d2 Merge pull request 'Say every forge event on the bus through a signed system webhook (hq issue 729)' (#304) from feat/729-the-forge-says-every-event-on-the-bus into main 2026-10-14 20:28:01 +00:00
jochen be53f5167a mesh-mail: bound a fetch by bytes, show mail_accounts only to its grantees, say why grants are unread, judge a secret without reading it (hq issue 733)
mesh/merge-gate out of date: judged against trunk 2130f9961291, the trunk is now 5f6a41146c3a; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 2130f9961291, the trunk is now 5f6a41146c3a; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
The review of #300 found a batch of 25 whole messages could hold a gigabyte,
mail_accounts told ungranted callers folder names, counts and server errors,
an unreadable grants setting was refused without its reason, and the watch
read the password only to learn it was there.
2026-10-14 22:24:11 +02:00
jschoubben 1d8a244ba4 Say every forge event on the bus through a signed system webhook, and announce a head or merge at once (novox/hq issue 729)
mesh/merge-gate out of date: judged against trunk 2edef5a00ae0, the trunk is now 0e7d9e74d22a; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 2edef5a00ae0, the trunk is now 0e7d9e74d22a; judged again on a recheck or when it is merged
mesh/delivery covered after its stop: a later walk that contains it was delivered: plan-1792011830935225963 (novox/mesh-catalog at 0361c44a into main, do…
The gitea module keeps one system webhook for every event family, made or
corrected at start with the admin account (POST /admin/hooks, forge 1.27.3)
and a secret it mints and keeps. Its receiver, at the docker bridge's
gateway, refuses an unsigned request before reading it, a body over 5 MB,
and a signature that does not verify; it answers a verified delivery at once,
drops one already heard by its delivery id, and publishes each as
forge.<kind>.<action> on the module's events (its own stream, ADR 0363,
follows issue 725). A pull request opened, pushed to or merged is announced
at once. The polls look every five minutes only while deliveries arrive, and
every 30 s again, saying so loudly, when one finds what no event said or ten
minutes pass without a delivery. The announced records keep both paths from
announcing twice.
2026-10-14 22:21:22 +02:00
mesh-admin 2edef5a00a Merge pull request 'Build build-agent and route-proxy without a build ID (hq issue 721)' (#301) from fix/a-module-moves-only-when-its-program-changes into main 2026-10-14 20:21:12 +00:00
jochen 2130f99612 mesh-mail: say mail events on its own stream MAIL, granted per account by grants (hq ADR 0363)
mesh/merge-gate head 2130f9961291 merged into trunk c71c513027e5 — pass: builds new: modules/mesh-mail, sent nowhere; no bus step; every machine composes w…
mesh/repo-check head 2130f9961291 merged into trunk c71c513027e5 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
ADR 0359 asked for a stream of its own and a bus grant from the module's
grants; ADR 0363 built both. The events leave the shared EVENTS stream, and
one family pattern per change replaces the literal per-account emits, so a
new account changes no manifest.
2026-10-14 22:17:09 +02:00
jochen ed5690cee4 Merge remote-tracking branch 'origin/main' into adr-359-mesh-mail 2026-10-14 22:16:50 +02:00
mesh-admin fadf5025cc Merge pull request 'Merge a pull request marked ready at its head once its checks pass (issue 722)' (#303) from feat/merge-on-ready into main 2026-10-14 19:57:00 +00:00
jschoubben baa5ef802e Name the keys gitea's mergeable config takes: none (ADR 0326)
mesh/merge-gate head baa5ef802e82 merged into trunk 7e3e62062bef — pass: builds gitea, mesh-delivery → novox; no bus step; every machine composes with the …
mesh/repo-check head baa5ef802e82 merged into trunk 7e3e62062bef — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
gitea now declares a setting, so every mergeable file of it must name its
keys; the runtime config reads no setting, and ready-authors reaches its
own file.
2026-10-14 21:50:05 +02:00
jschoubben 5b20e419ec Count a ready mark only from a listed account, after the last push and the label (issue 722)
mesh/merge-gate head 5b20e419ec1f merged into trunk 7e3e62062bef — fail: builds gitea, mesh-delivery → novox; no bus step; a manifest the change touches fa…
mesh/repo-check head 5b20e419ec1f merged into trunk 7e3e62062bef — fail: its merge-check.sh failed: files asking for a setting without saying whether it is…
mesh/delivery superseded: a newer head of the same pull request
Review of #303: anyone who can comment could turn a stale label into a mark,
and a long thread hid the newest comment. The forge now reads the pull
request's whole timeline and takes a "ready at" comment only from the
accounts in the trusted setting ready-authors (default the mesh's own forge
account and the operator's), written after the label was last added and after
the last push; any other author is ignored and said in the log.
2026-10-14 21:45:39 +02:00
mesh-admin 7e3e62062b Merge pull request 'mesh-catalog: hear the build seat that builds now (hq issue 712)' (#298) from fix-712-catalogue-hears-build-agent into main 2026-10-14 19:35:13 +00:00
jschoubben 7e0001e304 Merge a pull request marked ready at its head once its checks pass (issue 722)
mesh/merge-gate out of date: judged against trunk 482232e8c51c, the trunk is now 7e3e62062bef; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 482232e8c51c, the trunk is now 7e3e62062bef; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
The operator wants a reviewed pull request merged without a session calling
the merge tool again after every recheck. The reviewer marks it ready (label
mesh/ready and a comment "ready at <full head sha>"); the forge's open-pulls
read carries the mark, and mesh-delivery merges through gitea_merge_pull_request
at that head, in a group's order once its composed check passed. A push after
the mark clears it; a failed check is said on the pull request and as a
stalled line.
2026-10-14 21:23:26 +02:00
jschoubben d94764f001 Build build-agent and route-proxy without a build ID (hq issue 721)
mesh/merge-gate head d94764f00168 merged into trunk fadf5025cc5a — pass: builds build-agent, route-proxy → ace, g14, novox, shanks; no bus step; every mach…
mesh/repo-check head d94764f00168 merged into trunk fadf5025cc5a — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery covered after its stop: a later walk that contains it was delivered: plan-1792148283013560219 (novox/mesh-catalog at 3db85a0d into main, do…
mesh/delivery-group group fix/a-module-moves-only-when-its-program-changes delivered: every member is delivered
Go's build ID hashes every input package, so a change mesh-builder never reaches
still changed its bytes; without it, a rebuild of the same program is the same
file, and the check can say the module is unchanged.
2026-10-14 21:07:26 +02:00
mesh-admin 482232e8c5 Merge pull request 'Give the build agent a judge cache of its own (issue 704, ADR 0358)' (#297) from fix/704-release-branch-judge into main 2026-10-14 18:19:31 +00:00
jochen 70c1110c53 Add mesh-mail: watch the operator's own mailbox read-only and say each mail on the bus
mesh/merge-gate out of date: judged against trunk e22326240833, the trunk is now 482232e8c51c; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk e22326240833, the trunk is now 482232e8c51c; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
ADR 0359 decided one module holds the operator's mail accounts. This is its
first stage: an imap adapter that keeps IDLE on the inbox and polls the other
folders read-only, indexes each message in its own database, keeps each
attachment once by its SHA-256 in its bucket, and says every added, changed or
removed message as a metadata-only event. Bodies and attachments are served
only by its tools, to callers the trusted setting grants names. Its health
waits for the account's own secret until it is given at the desk (issue 710).
2026-10-14 20:14:42 +02:00
mesh-admin e223262408 Merge pull request 'An answered ask keeps its question whole, its answer below it (issue 714)' (#299) from fix/an-answered-ask-keeps-its-text into main 2026-10-14 18:11:06 +00:00
jochen dbe2949f84 mesh-catalog: pass over a build off its repository's trunk, as the controller does (ADR 0238)
mesh/merge-gate head dbe2949f8456 merged into trunk 482232e8c51c — pass: builds mesh-catalog → novox; no bus step; every machine composes with the change a…
mesh/repo-check head dbe2949f8456 merged into trunk 482232e8c51c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
A branch built through the build verb is a real build, and placing it would make it the catalogue's
current version though the controller never registers it. Say which of the controller's rules are
mirrored and which need its records.
2026-10-14 20:09:39 +02:00
jschoubben b7d9590fdc An answered ask keeps its question whole, its answer below it (issue 714)
mesh/merge-gate head b7d9590fdc95 merged into trunk e1c8965ddbf1 — pass: builds messenger, telegram → novox; no bus step; every machine composes with the c…
mesh/repo-check head b7d9590fdc95 merged into trunk e1c8965ddbf1 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Issue 372's fix with issue 639's one line edited an answered ask down to
'You chose X at HH:MM · headline', so the operator could not read back what
they approved. The headline stays the title, the summary, points, link and
Details stay the body, and how it ended and who acts on it become the note.
Telegram cuts the body from its end past 4096 characters, never the title or
the note.
2026-10-14 20:06:24 +02:00
jochen 7ee2869d60 mesh-catalog: narrow the placing by a string, which the check's non-strict compile can follow
mesh/merge-gate head 7ee2869d60eb merged into trunk e1c8965ddbf1 — pass: builds mesh-catalog → novox; no bus step; every machine composes with the change a…
mesh/repo-check head 7ee2869d60eb merged into trunk e1c8965ddbf1 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
2026-10-14 20:04:37 +02:00
jochen 0b74c04aa8 mesh-catalog: hear the build seat that builds now, so a merged build is registered when it is built (hq issue 712)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head 0b74c04aa89f merged into trunk e1c8965ddbf1 — pass: builds mesh-catalog → novox; no bus step; every machine composes with the change a…
mesh/repo-check head 0b74c04aa89f merged into trunk e1c8965ddbf1 — fail: merge-check.sh (go) passed; its merge-check-typescript.sh failed: TypeScript modul…
Since the build role moved to node-build-agent (ADR 0190), the catalogue consumed only the retired
mesh-build-machine.built, so builds reached it only through the replay it asks for at its start: mailu
e1c8965d, built two minutes after the last replay, was never registered. Consume node-build-agent.built,
and pass over the seat's merge checks, tries, dry runs and failures, which the old seat never said.
2026-10-14 20:03:25 +02:00
mesh-admin e1c8965ddb Merge pull request 'mailu: declare its unhealthy urgent (hq ADR 0355 §5, issue 691)' (#296) from adr-355-mailu-urgent into main 2026-10-14 17:01:22 +00:00
mesh-adminandClaude Opus 5.5 47ef2644ac Give the build agent a judge cache of its own, and prove a release branch's move puts its verdicts out of date
mesh/merge-gate head 47ef2644aca1 merged into trunk e22326240833 — pass: builds build-agent, gitea → ace, g14, novox, shanks; no bus step; every machine co…
mesh/repo-check head 47ef2644aca1 merged into trunk e22326240833 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/delivery-group group fix/704-release-branch-judge delivered: every member is delivered
Every merge check's judge is built where no pull request can reach (hq ADR 0358 §6,
issue 704): the build agent needs a directory beside its workspace, mounted at the
same path, that only judge builds mount. ADR 0358 §5 already holds in the forge's
holder; this test keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:48:30 +00:00
mesh-admin cb7bf94a77 Merge pull request 'mailu: ban clients that try to log in on port 25 (hq issue 708)' (#295) from fix-708-mailu-jail-port25 into main 2026-10-14 13:39:51 +00:00
jochen 8e821ef449 Say mail's failure urgently, on every channel
mesh/merge-gate head 8e821ef44992 merged into trunk cb7bf94a77ea — pass: builds mailu, sent nowhere; no bus step; 1 wait(s) for a person; every machine com…
mesh/repo-check head 8e821ef44992 merged into trunk cb7bf94a77ea — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
A broken mailu was a warning, said only on the desk; refused or stuck
mail must reach the operator wherever they are (ADR 0355 §5, issue 691).
2026-10-14 15:37:08 +02:00
jochen 07b9bd0ed4 Ban clients that try to log in on mail port 25
mesh/merge-gate head 07b9bd0ed44a merged into trunk 02427a60ef78 — pass: builds mailu, sent nowhere; no bus step; 1 wait(s) for a person; every machine com…
mesh/repo-check head 07b9bd0ed44a merged into trunk 02427a60ef78 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Port 25 takes no login, so a client that tries is guessing passwords;
mailu's jail only matched Dovecot's failures and never banned them
(issue 708). Relay attempts stay unbanned: postfix already refuses them.
2026-10-14 15:34:55 +02:00
mesh-admin 02427a60ef Merge pull request 'mailu: judge the smtp container by whether mail flows (hq issue 690)' (#294) from fix-690-mail-flows into main 2026-10-14 13:29:25 +00:00
jochen 8661bcfa22 Declare mailu's tools, so the controller knows the one its check asks
mesh/merge-gate head 8661bcfa2275 merged into trunk 50861c74f8b4 — pass: builds mailu, sent nowhere; no bus step; 1 wait(s) for a person; every machine com…
mesh/repo-check head 8661bcfa2275 merged into trunk 50861c74f8b4 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-14 15:21:39 +02:00
jochen 6049537655 Judge mailu's deferred queue by this side's faults, not one dead recipient
mesh/merge-gate head 60495376551d merged into trunk 50861c74f8b4 — fail: builds mailu, sent nowhere; no bus step; 1 wait(s) for a person; a manifest the ch…
mesh/repo-check head 60495376551d merged into trunk 50861c74f8b4 — fail: its merge-check.sh failed: files asking for a setting without saying whether it is…
mesh/delivery superseded: a newer head of the same pull request
A message to a far host that is down stays deferred for up to five days,
and mailu's unhealthy now pages the operator, so one dead recipient would
page for days. Overdue deferred mail is unhealthy only when a lookup failed
here (a name-service failure, as in issue 687) or when it reaches three or
more domains; one or two far domains are said in the answer, not alarmed.
2026-10-14 15:19:08 +02:00
jochen bf3e801f67 Judge mailu by whether mail flows, not only by its parts
mesh/merge-gate head bf3e801f6700 merged into trunk 50861c74f8b4 — fail: builds mailu, sent nowhere; no bus step; 1 wait(s) for a person; a manifest the ch…
mesh/repo-check head bf3e801f6700 merged into trunk 50861c74f8b4 — fail: its merge-check.sh failed: files asking for a setting without saying whether it is…
mesh/delivery superseded: a newer head of the same pull request
On 2026-10-14 every mailu container read healthy while postfix could not
resolve a name: inbound mail was refused with 450 and outbound mail sat in
the deferred queue for days (issue 690). The smtp container is now judged
by a tool check, mailu_health, asked inside it: postfix runs (the image's
own check, kept), no message waits deferred longer than 30 minutes, and a
public mail host resolves through the resolver postfix is given.
2026-10-14 15:14:51 +02:00
mesh-admin 50861c74f8 Merge pull request 'mailu: judge the front by SMTP and its certificate, not only its web health (hq issue 688)' (#285) from fix-688-mailu-health into main 2026-10-14 13:06:45 +00:00
mesh-admin b15846dd6e Merge pull request 'mailu: move the resolver to Mailu 2024.06.63, which follows the root key change (hq issue 687)' (#284) from fix-687-mailu-resolver into main 2026-10-14 12:32:36 +00:00
mesh-admin a44d490d97 Merge pull request 'Five postgres consumers say which provision their check exercises (hq issue 145, to-be 48 Phase E)' (#289) from fix/145-a-consumer-is-held-under-its-provider into main 2026-10-14 05:26:47 +00:00
mesh-admin af5cf53a34 Merge pull request 'lab: name the base image's age, refuse a stale one, and rebuild it (issue 698)' (#288) from fix/698-the-lab-names-its-base-age into main 2026-10-14 05:12:37 +00:00
jochen 2bb5d344f6 Hold umami's check to the share route's own 404, so an unknown route cannot pass it (hq issue 145, review 262)
mesh/merge-gate head 2bb5d344f6f1 merged into trunk af5cf53a3450 — pass: builds gitea, mesh-knowledge, n8n, nextcloud, umami → ace, novox; no bus step; eve…
mesh/repo-check head 2bb5d344f6f1 merged into trunk af5cf53a3450 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery covered: a later merge that contains it was delivered: novox/mesh-catalog@054dc9618ce3 (merged as b15846dd into main, walk plan-17919811644…
mesh/delivery-group group fix/145-a-consumer-is-held-under-its-provider delivered: every member is delivered
Any route umami does not serve answers 404 too, as an HTML page. The
share route answers JSON with code not-found only after the database
was asked; with the database down it answers 500.
2026-10-14 07:07:11 +02:00
mesh-admin e846c289c8 Merge pull request 'lab: a run whose suite proved nothing reads as failed, with its skips (issue 693)' (#287) from fix/693-a-run-that-proved-nothing-is-not-passed into main 2026-10-14 05:05:40 +00:00
jochen 0fcc560fec lab: one job at a time with no await between check and start, and no rebuild left holding anything (issue 698, review 256)
mesh/merge-gate head 0fcc560fecd6 merged into trunk e846c289c878 — pass: builds lab → shanks; no bus step; every machine composes with the change as it did…
mesh/repo-check head 0fcc560fecd6 merged into trunk e846c289c878 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
A run and a rebuild each asked the package registry between checking for
the other and writing their status, so both could start and a bed could
raise machines from an image being replaced. The registry is asked first
now. A hard-killed rebuild's credential is cleaned like a run's; a job that
never recorded its process is lost rather than blocking every run; and
lab_check reports a missing or unreadable base image.
2026-10-14 03:34:46 +02:00
jochen acf8a76646 lab: read the suite's verdict only after its recorded line, NOT REACHED in any case (issue 693, review 254)
mesh/merge-gate head acf8a76646de merged into trunk ebf7f3773dc7 — pass: builds lab → shanks; no bus step; every machine composes with the change as it did…
mesh/repo-check head acf8a76646de merged into trunk ebf7f3773dc7 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/delivery-group group fix/693-a-run-that-proved-nothing-is-not-passed delivered: every member is delivered
A test's own output, printed before the suite reports, could carry a line
shaped like the suite's NOT PASSED or skipped line and stand for its verdict.
2026-10-14 03:28:20 +02:00
jochen b62bb15839 Say which provision five consumers' checks exercise, so a database down is one condition (hq issue 145)
mesh/merge-gate out of date: judged against trunk ebf7f3773dc7, the trunk is now e846c289c878; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk ebf7f3773dc7, the trunk is now e846c289c878; judged again on a recheck or when it is merged
mesh/delivery-group group fix/145-a-consumer-is-held-under-its-provider checking: composed check out of date: novox/mesh-catalog@b62bb158398d was judged agains…
mesh/delivery superseded: a newer head of the same pull request
gitea, nextcloud, umami and n8n gain an http check that reaches their
database through their own endpoint; mesh-knowledge's watch already
asks the database and now names it. While postgres is unhealthy, their
findings are held under its condition instead of raising one each.
2026-10-14 03:21:53 +02:00
jochen ff9d3ee6a0 lab: name the base image's age, refuse a stale one, and rebuild it (issue 698)
mesh/merge-gate head ff9d3ee6a06c merged into trunk ebf7f3773dc7 — pass: builds lab → shanks; no bus step; every machine composes with the change as it did…
mesh/repo-check head ff9d3ee6a06c merged into trunk ebf7f3773dc7 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
A bed machine carries the package database its base image was built with,
and the mirrors stop serving it within weeks: genesis-keeps-its-bus failed
on a 404 from an image 44 days old, with nothing saying its age and no mesh
tool to rebuild it. lab_check and lab_run now say the age, lab_run refuses
an image older than 14 days and names the remedy, and lab_base reads the
image and rebuilds it with mesh-lab's base build when asked.
2026-10-14 03:21:23 +02:00
jochen c8255eb15d lab: a run whose suite proved nothing reads as failed, with its skips (issue 693)
mesh/merge-gate head c8255eb15d12 merged into trunk ebf7f3773dc7 — pass: builds lab → shanks; no bus step; every machine composes with the change as it did…
mesh/repo-check head c8255eb15d12 merged into trunk ebf7f3773dc7 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery-group group fix/693-a-run-that-proved-nothing-is-not-passed checking: 1 of 2 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
lab_status took the suite's exit as the verdict, and the runner exits 0 when
every test skipped: a genesis bed that skipped its only test as NOT REACHED was
recorded as passed. A run that ended now has its outcome read from its log and
kept in its status - totals, each skip with its reason, and why it did not pass
- and one whose exit said 0 while no test passed reads as failed. Read from the
lab's own lines when it prints them, from the runner's summary for a lab of
before.
2026-10-14 03:14:06 +02:00
jochen 4ded07bc63 Judge mailu's front by what mail needs, not only its web health
mesh/merge-gate head 4ded07bc635f merged into trunk b15846dd6e78 — pass: builds mailu, sent nowhere; no bus step; 1 wait(s) for a person; every machine com…
mesh/repo-check head 4ded07bc635f merged into trunk b15846dd6e78 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The image's own check asks nginx's /health and that dovecot runs, so the
front stayed healthy while mail could not flow (issue 688). It now also
needs port 25 to greet over SMTP and the served certificate to hold for
14 more days; certbot renews at 30, so a renewal stuck for two weeks is
said long before the certificate expires.
2026-10-14 02:19:54 +02:00
jochen 054dc9618c Move mailu's resolver to Mailu 2024.06.63, which follows the root key change
mesh/merge-gate head 054dc9618ce3 merged into trunk a44d490d974c — pass: builds mailu, sent nowhere; no bus step; 1 wait(s) for a person; every machine com…
mesh/repo-check head 054dc9618ce3 merged into trunk a44d490d974c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The root zone signs with KSK-2024 since 2026-10-11. The pinned resolver
image (2024.06.59) trusted only KSK-2017 and kept its anchor inside the
container, so every lookup failed with SERVFAIL and mail was refused or
stuck (issue 687). 2024.06.63 refreshes the anchor with unbound-anchor at
every start and fails its health check on SERVFAIL, which the old
'dig @127.0.0.1' never did (issue 688).
2026-10-14 02:17:25 +02:00
mesh-admin ebf7f3773d Merge pull request 'build-agent: declare busy, so a replaced agent finishes the check it runs (issue 594, hq ADR 0335)' (#283) from feat/335-the-build-agent-declares-busy into main 2026-10-13 22:39:50 +00:00
jschoubben c876ad4a83 Merge remote-tracking branch 'origin/main' into feat/335-the-build-agent-declares-busy
mesh/merge-gate head c876ad4a8303 merged into trunk 23a2e1e20467 — pass: builds build-agent → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head c876ad4a8303 merged into trunk 23a2e1e20467 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
# Conflicts:
#	modules/build-agent/README.md
2026-10-14 00:11:50 +02:00
jschoubben 12a5e7be73 build-agent: name mesh-builder by its path in busy, as its health does (review of #260)
mesh/merge-gate head 12a5e7be7321 merged into trunk 23a2e1e20467 — fail: builds build-agent → ace, g14, novox, shanks; no bus step; head 12a5e7be7321 does …
mesh/repo-check head 12a5e7be7321 merged into trunk 23a2e1e20467 — fail: head 12a5e7be7321 does not merge cleanly into trunk 23a2e1e20467: in conflict modu…
mesh/delivery superseded: a newer head of the same pull request
2026-10-14 00:07:42 +02:00
mesh-admin 23a2e1e204 Merge pull request 'mesh-delivery: a closed pull request's check builds go, and a walk says what each build did (ADR 0350, issue 649)' (#282) from feat/350-one-build-per-change into main 2026-10-13 21:30:30 +00:00
jschoubben 66d22dd53e Let a closed pull request's check builds go, and show what each walk build did
mesh/merge-gate head 66d22dd53eff merged into trunk 7ed119995f69 — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head 66d22dd53eff merged into trunk 7ed119995f69 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/delivery-group group feat/350-one-build-per-change delivered: every member is delivered
A check's builds for its merge have no taker once the pull request closes
unmerged, and the delivery should say whether a module was reused from the
check or built, and why (novox/hq ADR 0350 §4, §5).
2026-10-13 23:04:41 +02:00
mesh-admin 7ed119995f Merge pull request 'gitea serves the registry's public root; the lab trusts a tarball only there or at the forge's own origin (issue 624, ADR 0346)' (#280) from fix/624-the-forge-serves-its-public-root into main 2026-10-13 19:52:05 +00:00
mesh-admin faebc3569a Merge pull request 'An ask reads at a glance on the phone: a headline, a summary and its points, the rest in a note (issues 383 and 639)' (#278) from fix/383-an-ask-reads-at-a-glance into main 2026-10-13 19:02:21 +00:00
jochen 1faee6e279 lab: a forge setting that is not http(s) is never a trusted origin (hq issue 624, review 222)
mesh/merge-gate head 1faee6e27976 merged into trunk faebc3569acf — pass: builds gitea, lab → novox, shanks; no bus step; every machine composes with the ch…
mesh/repo-check head 1faee6e27976 merged into trunk faebc3569acf — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
2026-10-13 20:33:13 +02:00
jochen 6f7d06726c gitea serves the registry's public root, and the lab trusts a tarball only at it or the forge's own origin (hq ADR 0346, issue 624)
The forge writes every tarball at its public name and the lab reaches it at
a private address, so npm 12 refused every locked @novox package. The public
name now comes from the mesh's own composition of the forge's route, through
the lab's binding, and the lab compares scheme, host and port.
2026-10-13 20:33:02 +02:00
jochen f09daabec3 A withheld change line in the note keeps its key's name; SDK at 5933eb7 (the review of mesh-controller #304)
mesh/merge-gate head f09daabec3a8 merged into trunk 9c166398f204 — pass: builds claude-code, desk-channel, mesh-knowledge, messenger, telegram → ace, g14, …
mesh/repo-check head f09daabec3a8 merged into trunk 9c166398f204 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/delivery-group group fix/383-an-ask-reads-at-a-glance delivered: every member is delivered
The router's fallback said a refused note line as "(a line carrying … is withheld here)", so the operator
could not tell which setting held a value they were not shown. A change's line now keeps its key; any other
line, or one whose key is refused too, is said withheld whole. The SDK pin moves to mesh-sdk 5933eb7, where
no two asks digest the same.
2026-10-13 20:32:50 +02:00
jochen bb5a342863 Every value an approval sets is in the note, whole where the sender is proven; the note is kept whole (issue 383)
mesh/merge-gate head bb5a342863ee merged into trunk 9c166398f204 — pass: builds claude-code, desk-channel, mesh-knowledge, messenger, telegram → ace, g14, …
mesh/repo-check head bb5a342863ee merged into trunk 9c166398f204 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery-group group fix/383-an-ask-reads-at-a-glance checking: 1 of 2 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
The review of mesh-controller #304: a proposal of more than three keys showed only the names of the rest,
and its Details could carry no value, so Approve bound values the operator never saw. The router now
shows an ask's WholeDetails in the note in place of its Details on a kind that proves who answers, held
to the secret rule alone, and refuses an ask whose whole details carry a secret's shape. Telegram keeps
the note whole when a message is too long, cutting the body first and saying so, and escapes the note
as every other part. SDK at mesh-sdk f474544.
2026-10-13 17:16:50 +02:00
jochen eadf301f9f An ask reads at a glance on the phone: a headline, a summary and its points, the rest in a note (issues 383 and 639)
The operator would not read the Telegram ask for ADR 0343: thirty plain lines, every clause on one, no bold,
no summary. A settings proposal was the same wall of text, its values masked so the approval was blind.

- messenger: an ask's message is the asker's headline and explanation alone; who asked and when, what each
  answer does, the expiry, where else it is answered and the asker's Details travel as the message's note
  (asks.Message.Note). Answered, the message is one line, "You chose Approve at 16:06 on Telegram ·
  <headline>". An explanation that does not read at a glance (asks.Glance) is shown and logged. On a kind
  that proves who answers, an ask's values are shown whole (asks.Ask.Whole), held to the secret rule alone
  (from the earlier work on issue 383, #178).
- telegram: every message in HTML, never reworded: the title bold, a key line monospace, the note apart in
  a collapsed blockquote; markup escaped. A tap is compared by its words, whitespace aside, and a message
  shown before is still compared by the digest it was kept under.
- desk-channel: the note follows the body.
- mesh-knowledge: a record that needs the operator carries a brief (headline, summary, at most three
  points), the 30th per-record rule; its ask shows the brief and never the clauses. An ask made before is
  composed again as it was asked (form), so its warrant still holds.
- claude-code: an instructions proposal's explanation is one summary line (the proposer's, or the
  module's), its text in the Details; a question's context is at most 300 characters and its recommendation
  one point. Older proposals and questions keep their form.

The SDK is pinned at mesh-sdk 9588871 (fix/639-an-ask-reads-at-a-glance), which adds Message.Note and
asks.Glance.
2026-10-13 17:16:50 +02:00
mesh-admin 9c166398f2 Merge pull request 'Revert #277: no showmissed lock in betterbird's policy (issue 640)' (#281) from fix/640-revert-the-showmissed-lock into main 2026-10-13 15:06:09 +00:00
mesh-admin e209d46faf Merge pull request 'claude-code: the memory watch only notes a change, so it starts on a node with hundreds of files (issue 367)' (#279) from fix/367-memory-watch-takes-fast into main 2026-10-13 14:58:35 +00:00
jochen f1f866050e Revert the showmissed lock of #277: the operator wants no special handling in betterbird's config (hq issue 640)
mesh/merge-gate head f1f866050eee merged into trunk e209d46faf3d — pass: builds betterbird, sent nowhere; no bus step; 2 wait(s) for a person; every machin…
mesh/repo-check head f1f866050eee merged into trunk e209d46faf3d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The fix is the approved settings layer (proposal s09bf8c99da7ac8b9:
calendar.alarms.showmissed false), pushed to g14 and shanks.
2026-10-13 16:54:51 +02:00
mesh-admin 4d15a155e6 Merge pull request 'betterbird: a passed meeting's reminder does not come back at every Owl reload (issue 640)' (#277) from fix/640-a-dismissed-reminder-stays-dismissed into main 2026-10-13 14:47:58 +00:00
jschoubben e46eeb8298 claude-code: the memory watch only notes a change, so it starts on a node with hundreds of files (issue 367)
mesh/merge-gate head e46eeb829853 merged into trunk 4d15a155e634 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head e46eeb829853 merged into trunk 4d15a155e634 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The runtime hands a watch its current values one at a time and gives them ten
seconds. The handler read the state, waited for a running look and wrote files
for each value, so with about 450 memory values the watch never started on g14
or shanks, and a file written on g14 never reached shanks. The handler now
notes each key's newest revision and returns; a worker applies what was noted
at the state's current version.
2026-10-13 16:40:41 +02:00
mesh-admin 5f491e0c09 Merge pull request 'lab: name the package registry by the address it names itself by, so npm 12 installs locked @novox packages (issue 624)' (#275) from fix/624-lab-names-the-registry-as-it-names-itself into main 2026-10-13 14:35:04 +00:00
jochen 73404a3c3f betterbird: lock the missed-reminder pass off, so a passed meeting's reminder does not come back (hq issue 640)
mesh/merge-gate head 73404a3c3f21 merged into trunk 5f491e0c097d — pass: builds betterbird, sent nowhere; no bus step; 2 wait(s) for a person; every machin…
mesh/repo-check head 73404a3c3f21 merged into trunk 5f491e0c097d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
With calendar.alarms.showmissed on, Betterbird's alarm service fires every
reminder of the past month that carries no later acknowledgement at each
load of a calendar. Owl never sends a dismissal to Exchange and keeps it only
in Betterbird's local cache, so on g14 September's Exchange meetings came
back about every ten minutes, Dismiss or not. The policy now locks the pass
off whatever the setting or profile says, and betterbird_check says when the
running Betterbird still has it on beside an Owl calendar.
2026-10-13 16:32:17 +02:00
mesh-admin 305c2ba247 Merge pull request 'claude-code: a session's memory follows the operator to every node (issue 367, ADR 0343)' (#276) from fix/367-memory-follows-the-operator into main 2026-10-13 14:26:59 +00:00
jochen 8ad121d5c3 lab: take the registry's address only from the forge's own host, built from the parsed URL (hq issue 624, review of #275)
mesh/merge-gate head 8ad121d5c369 merged into trunk 305c2ba24733 — pass: builds lab → shanks; no bus step; every machine composes with the change as it did…
mesh/repo-check head 8ad121d5c369 merged into trunk 305c2ba24733 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
What the registry answers may come over plain HTTP, so it must not choose
where packages come from, nor reach the run's npm configuration as a raw
string.
2026-10-13 16:26:48 +02:00
jschoubben 305601d25e claude-code: memory carrying never leaves a memory folder, never takes the unreadable for gone, and screens every write (issue 367, review of #276)
mesh/merge-gate head 305601d25ef4 merged into trunk a84d0094a0a3 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head 305601d25ef4 merged into trunk a84d0094a0a3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
A value in the state could name a project such as ../../.. and be written outside
the memory folder; a file or folder that could not be read was carried as a
removal; text kept over a removal or merged in a conflict reached the state
unscreened; and a conflict whose file could not be written was merged into the
state again on every look. Names are judged before any file is touched, what
cannot be read is kept and listed, every state write is screened, a conflict is
written to the file before the state and warns once when it cannot be, a file
changed between reading and writing is not written over, non-UTF-8 text is not
carried, and a node that was away takes a removal it holds no change against
(ADR 0343 §9, as clarified).
2026-10-13 16:20:02 +02:00
jschoubben 910bc58fe0 claude-code: keep two memory versions too large for one file in two, and say a text that cannot fit one value (issue 367)
mesh/merge-gate head 910bc58fe01d merged into trunk a84d0094a0a3 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head 910bc58fe01d merged into trunk a84d0094a0a3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
A merged file could exceed the state's 256 KiB value; the newer now stays in the
file and the older is written beside it, each carried. A node that was away takes
the newest version when its own file holds no uncarried change (tested).
2026-10-13 16:03:37 +02:00
jschoubben 0096beeff9 claude-code: say a memory conflict in the operator's plain words, and skip a look the state does not answer (issue 367)
mesh/merge-gate head 0096beeff999 merged into trunk a84d0094a0a3 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head 0096beeff999 merged into trunk a84d0094a0a3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
The warning now opens with what the operator is needed for and names no path,
agent or session (ADR 0253's plain rule); clause references follow ADR 0343 as
reviewed.
2026-10-13 16:01:16 +02:00
jschoubben 6aa30eae66 claude-code: carry each project's memory folder between nodes (issue 367, ADR 0343)
mesh/merge-gate head 6aa30eae66be merged into trunk a84d0094a0a3 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head 6aa30eae66be merged into trunk a84d0094a0a3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
A session started on another node began without what the sessions elsewhere had
learned, because Claude Code keeps project memory on one node. The module's own
process now carries the memory folders through its kept memory state: a change
is written elsewhere only over the version it replaced, a file changed on two
nodes keeps both (newer first) with a warning, and every replaced or removed
version is kept. No tool writes memory.
2026-10-13 15:57:44 +02:00
jochen bb60de6d61 lab: name the package registry by the address it names itself by, so npm 12 installs locked @novox packages (hq issue 624)
mesh/merge-gate head bb60de6d616a merged into trunk a84d0094a0a3 — pass: builds lab → shanks; no bus step; every machine composes with the change as it did…
mesh/repo-check head bb60de6d616a merged into trunk a84d0094a0a3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
npm 12 installs a locked tarball only from its configured registry; the forge
writes every tarball URL, and so every lock file, at its public address, and
the lab named the registry by its forge setting's address. Every run stopped
at mesh-tools' install with EALLOWREMOTE.
2026-10-13 15:25:41 +02:00
mesh-admin a84d0094a0 Merge pull request 'lab: read the bound forge credential through the tools bundle's env, and check in a fresh directory each time (issue 624)' (#274) from fix/624-lab-check-in-a-fresh-directory into main 2026-10-13 12:58:15 +00:00
jochen dd39d4ace7 lab: name the bound credential in the tools bundle's env, and skip the locked-tree test as root (hq issue 624, reviews 198, 199)
mesh/merge-gate head dd39d4ace74b merged into trunk 0d723484a84a — pass: builds lab → shanks; no bus step; every machine composes with the change as it did…
mesh/repo-check head dd39d4ace74b merged into trunk 0d723484a84a — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The lab's tools read their bound npm-package-registry credential, root's at 0600 on the machine. The
controller gives the runtime's account exactly the files a tools bundle names in its env (ADR 0192), as
gitea and umami are given theirs, so the bundle now names the credential and its binding there, and no
other credential changes hands.

The merge check runs as root, where no directory mode stops a removal: the test that makes an
unremovable tree now skips there before it makes anything, and a second test clears leftovers whoever
runs it.
2026-10-13 14:51:18 +02:00
jochen 5bd40fa406 lab_check: work in a fresh directory each time, and name what an earlier check left (hq issue 624)
mesh/merge-gate head 5bd40fa4061d merged into trunk 0d723484a84a — pass: builds lab → shanks; no bus step; every machine composes with the change as it did…
mesh/repo-check head 5bd40fa4061d merged into trunk 0d723484a84a — fail: merge-check.sh (go) passed; its merge-check-typescript.sh failed: TypeScript modul…
mesh/delivery superseded: a newer head of the same pull request
lab_check cloned into one fixed path, <work>/check, after an rm -rf whose result it ignored; a tree an
earlier check left that the lab's account could not remove made every check fail on "destination path
already exists". Each check now works in a fresh check-* directory it removes when done, and clears
what earlier checks left, naming any path it could not remove.
2026-10-13 14:40:06 +02:00
mesh-admin 0d723484a8 Merge pull request 'lab: clone the forge with the credential the mesh binds and seals to the lab (issue 624)' (#272) from fix/624-lab-reads-the-forge-with-its-credential into main 2026-10-13 12:07:06 +00:00
jochen 8d8dcd5ebb lab: a read-only forge account of its own, and no credential left on disk after a stop (hq issue 624, review 196)
mesh/merge-gate head 8d8dcd5ebb86 merged into trunk cd239283207a — pass: builds gitea, lab → novox, shanks; no bus step; every machine composes with the ch…
mesh/repo-check head 8d8dcd5ebb86 merged into trunk cd239283207a — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The lab's credential came from the package team, which also writes the @novox packages, and the
branches a run clones are not reviewed yet. The lab now contributes {"access": "read"} to its
npm-package-registry requirement, and gitea's provisioner puts such a consumer on a team of its own,
readers: code read on the org's repositories, package read, no write, and off the package team. The
build agent, which publishes, stays on packages.

A stop's SIGTERM to the run's process group killed the wrapper before it removed the credential file.
The run's script now removes it as soon as its clones are done and traps every way it ends, and a run's
start and lab_check remove what a crash or a reboot left.
2026-10-13 14:00:59 +02:00
mesh-admin cd23928320 Merge pull request 'licence manager: say what moved a binding, why, and who asked (issue 625)' (#270) from fix/625-a-licence-move-says-what-why-and-who into main 2026-10-13 11:59:02 +00:00
mesh-admin 8922c89059 Merge pull request 'dev-tools: give every node the toolchains to build and test the mesh's code (issue 626)' (#269) from fix/626-every-node-has-the-dev-tools into main 2026-10-13 11:56:18 +00:00
jochen 789d2d9d64 lab: clone the forge with the credential the mesh binds and seals to the lab (hq issue 624)
mesh/merge-gate out of date: judged against trunk 19ecad261cf2, the trunk is now 8922c890595b; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 19ecad261cf2, the trunk is now 8922c890595b; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
mesh-tools-go is private, and the lab cloned over HTTP with no credential: a run failed at its checkout,
"could not read Username". The lab now requires the package registry, as the build agent does; the
forge's provisioner binds it an account and seals that account's password, and a run offers them to
the forge's own origin through git's credential store, from a file beside the run and removed when it
ends: never in a URL, the run's script, an argument or the log. lab_check reads every repository a run
clones with that same credential and names one it cannot read.
2026-10-13 13:54:16 +02:00
mesh-admin 1725ffc799 licence manager: a login's adoption is said beside the bindings it made, not first (issue 625)
mesh/merge-gate head 1725ffc799bd merged into trunk 8922c890595b — pass: builds claude-code, claude-licence-manager → ace, g14, novox, shanks; no bus step;…
mesh/repo-check head 1725ffc799bd merged into trunk 8922c890595b — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The bindings a login makes are now announced as licence.bound before the adoption's own event, so the test asks for each event rather than for adoption to come first.
2026-10-13 11:49:30 +00:00
mesh-admin a72eb3dbb2 licence manager: say what moved a binding, why, and who asked (issue 625)
mesh/merge-gate head a72eb3dbb28d merged into trunk 19ecad261cf2 — pass: builds claude-code, claude-licence-manager → ace, g14, novox, shanks; no bus step;…
mesh/repo-check head a72eb3dbb28d merged into trunk 19ecad261cf2 — fail: its merge-check.sh failed: --- FAIL: TestAManagerWithNoLicenceAdoptsTheNewestLogin…
mesh/delivery superseded: a newer head of the same pull request
A switch of g14 back to another licence on 2026-10-13 left no trace: the manager logged no line and sent no event for bind, switch or release, and its audit kept only the verb's name. Every change of what a consumer holds now goes to the journal, the audit and a licence.<what> event with the same fields: consumer, licence before and after, generation, why (the verb or the rule) and by (the caller the runtime proved, or the manager itself). The node is told why in the bindings state and says it when it applies. The audit and binding tables become readable through the store's reader. The SDK moves to the version that proves the caller (issue 365).
2026-10-13 11:45:52 +00:00
mesh-admin c0726ac264 dev-tools: give every node the toolchains to build and test the mesh's code (issue 626)
mesh/merge-gate head c0726ac2641b merged into trunk 19ecad261cf2 — pass: builds new: modules/dev-tools, sent nowhere; no bus step; every machine composes w…
mesh/repo-check head c0726ac2641b merged into trunk 19ecad261cf2 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Every node is a development machine: an agent builds a change and runs its tests there before opening a pull request, and the lab tests it afterwards as QA. No module declared Go or npm, so no node could do the first part.
2026-10-13 11:42:12 +00:00
mesh-admin 19ecad261c Merge pull request 'lab: install each checkout where its lock file is, check out mesh-tools-go, and have lab_check catch both (issue 624)' (#268) from fix/624-lab-run-installs-what-each-checkout-has into main 2026-10-13 11:11:58 +00:00
jochen c6a9c52dc7 Install each lab checkout where its lock file is, check out mesh-tools-go, and have lab_check catch both (hq issue 624)
mesh/merge-gate head c6a9c52dc7f2 merged into trunk 45d01e77c26d — pass: builds lab → shanks; no bus step; every machine composes with the change as it did…
mesh/repo-check head c6a9c52dc7f2 merged into trunk 45d01e77c26d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
lab_run ran npm ci at the root of mesh-sdk, mesh-tools and mesh-lab; mesh-tools keeps its package in
node-tools/ with no lock file at its root, so every run failed at its setup in nine seconds. A run now
installs (and, for the SDK and the base, builds) where a package-lock.json is: the root, or each
directory one level down; a package.json with no lock file fails the run naming it. It checks out
mesh-tools-go, which genesis builds since issue 616, and tells the beds where its checkouts are
(MESH_LAB_REPOS). lab_check asks the forge for every repository a run clones and applies the same install
rule to the ones it installs, so a run's setup failure is found before a run.
2026-10-13 11:55:01 +02:00
mesh-admin 45d01e77c2 Merge pull request 'fail2ban: know the own-addresses list in the form fail2ban prints it (hq issue 238)' (#267) from fix/238-fail2bans-own-file-form into main 2026-10-13 09:18:26 +00:00
jochen c78d0a0134 fail2ban: know the own-addresses list in the form fail2ban prints it (hq issue 238)
mesh/merge-gate head c78d0a01342c merged into trunk f35b06750452 — pass: builds fail2ban → ace, g14, novox, shanks; no bus step; every machine composes wit…
mesh/repo-check head c78d0a01342c merged into trunk f35b06750452 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
fail2ban reads the ignoreip entry file:/var/lib/... and prints it back as file://var/lib/...,
in get <jail> ignoreip and in its "Ignore ... by ..." log lines alike. The watch compared the
text, so on ace it said every jail lacked the list while each held it, failing fail2ban's health
and its walk; and it would never have counted a failure from the operator's own address. Compare
the path instead, with a test on fail2ban's real form.
2026-10-13 11:14:24 +02:00
mesh-admin f35b067504 Merge pull request 'fail2ban: never ban the operator's own addresses, and say a failure from one (hq issue 238, ADR 0339)' (#266) from fix/238-the-operators-own-addresses into main 2026-10-13 09:05:49 +00:00
jochen 93d58f3740 fail2ban: never ban the operator's own addresses, and say a failure from one (hq issue 238, ADR 0339)
mesh/merge-gate head 93d58f3740a0 merged into trunk 5cd608ba8898 — pass: builds fail2ban → ace, g14, novox, shanks; no bus step; every machine composes wit…
mesh/repo-check head 93d58f3740a0 merged into trunk 5cd608ba8898 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1791883115921573192 (novox/mesh-catalog at 45d01e77 into main,…
An agent polling the forge from the operator's home got that home banned for a day and then four
weeks, and nothing in the house reached the control-node. Every jail spared only private ranges.

The mesh-wide setting own-addresses reaches the machine as a mergeable file whose own content is
the empty list, so the module composes while it is unset. A new watch process (root) writes its
entries into a list every jail's base ignoreip names as file:, which fail2ban reloads without a
restart; it checks every jail names that list and keeps each failure a jail ignored because of it
for a day. The new fail2ban_health tool judges the watch, so the controller raises a warning
naming node, jail, address and count. The ban verb refuses an address on the setting or on the
jail's ignore list before the daemon is asked.
2026-10-13 10:54:49 +02:00
mesh-admin 5cd608ba88 Merge pull request 'Deliver a merge whose walk said its modules matched what runs, instead of stopping it as stranded (hq issue 611)' (#265) from fix/611-an-unchanged-build-is-delivered into main 2026-10-13 08:25:47 +00:00
jochen 2f5e97030a Deliver a merge whose walk said its modules matched what runs, instead of stopping it as stranded (hq issue 611)
mesh/merge-gate head 2f5e97030a5c merged into trunk bc3b59227f46 — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head 2f5e97030a5c merged into trunk bc3b59227f46 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/delivery-group group fix/611-an-unchanged-build-is-delivered delivered: every member is delivered
A walk that moved nothing because every module's recorded build source held none of the changed files now
says so per module (delivery.unchanged). Such a merge moves nothing (ADR 0270 §1): it is delivered, naming
the modules, not stopped 20 minutes later with the operator asked about a change already on every machine.
A module the walk neither built nor said unchanged still stops it (issue 474).
2026-10-13 08:18:25 +02:00
mesh-admin bc3b59227f Merge pull request 'A password, token or API key is the vault's, never a setting (hq issue 342)' (#264) from fix/342-a-secret-is-never-a-setting into main 2026-10-13 05:52:05 +00:00
jochen eb42806379 Make grafana prefer the vault's password, and let the secrets list only shrink (hq issue 342)
mesh/merge-gate head eb4280637906 merged into trunk 53e51864969f — pass: builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, n…
mesh/repo-check head eb4280637906 merged into trunk 53e51864969f — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery delivered
mesh/delivery-group group fix/342-a-secret-is-never-a-setting delivered: every member is delivered
grafana's client logged in with a token setting ahead of the vault's admin password,
and its runtime config let a layer set token. The token setting is now ignored and
said, token is protected, and the vault's password comes first.

checks/secrets/known could gain a line hiding a newly unprotected key. merge-check.sh
now judges it against the trunk's copy at the merge base, and refuses an added line.
2026-10-13 07:43:08 +02:00
jochen fe2040c65e Keep every password, token and API key the vault's, never a setting (hq issue 342)
mesh/merge-gate head fe2040c65ea2 merged into trunk 53e51864969f — pass: builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, n…
mesh/repo-check head fe2040c65ea2 merged into trunk 53e51864969f — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery-group group fix/342-a-secret-is-never-a-setting ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
A setting outranked the vault: baserow, grafana, letta and unifi merged a layer's
password over the ${secret:...} in their runtime config, and the gitea,
home-assistant, icecast, influxdb, mailu and nextcloud clients read the settings
file before the vault's file. A secret in a setting cannot be rotated and is read
back by the settings verb.

The four files now keep their password protected, so the controller refuses a
setting of it. The six clients ignore a secret in the settings file and say so
once, by key and never with the value; icecast reads its admin password from the
vault's file. checks/secrets refuses a mergeable file whose secret a layer could
replace; searxng's nested server.secret_key is listed as known until the
controller protects a nested key.
2026-10-13 07:32:19 +02:00
mesh-admin 53e5186496 Merge pull request 'build-agent: the package registry is optional, and genesis hands it the forge it raised (hq issue 121, ADR 0336)' (#263) from fix/121-genesis-package-registry into main 2026-10-13 03:37:08 +00:00
jochen e0bc919d49 Let genesis assign the build agent before the package registry exists (hq issue 121, ADR 0336)
mesh/merge-gate head e0bc919d4945 merged into trunk f88e3452f8b8 — pass: builds build-agent → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head e0bc919d4945 merged into trunk f88e3452f8b8 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery covered: a later merge that contains it was delivered: novox/mesh-catalog@2f5e97030a5c (merged as 5cd608ba into main, walk plan-17918799551…
mesh/delivery-group group fix/121-genesis-package-registry delivered: every member is delivered
build-agent's npm-package-registry becomes optional, so genesis's step 13 is no
longer refused with 'nothing provides npm-package-registry'. Until gitea is
assigned, the agent on genesis's node reads the genesis-package-registry
own-secret the installer seals at the packages step; it is issued outside the
mesh, so no other machine is given one. The binding and its credential are
composed only while bound (ADR 0330 §6), and no file names them otherwise.
2026-10-13 02:50:39 +02:00
mesh-admin f88e3452f8 Merge pull request 'betterbird: own Betterbird's whole configuration as settings, shown without a credential (hq issue 586, ADR 0329)' (#262) from fix/586-betterbird-owns-its-configuration into main 2026-10-13 00:25:41 +00:00
mesh-admin 827b89dbf1 Merge pull request 'Mark grafana's and home-assistant's soft requirements optional (hq issue 166, ADR 0330)' (#259) from fix/166-optional-requirements into main 2026-10-13 00:22:35 +00:00
jochen c9a711c259 betterbird: write the account lists only while the profile is closed, and never propose over an unreadable layer (hq issue 586, review 121)
mesh/merge-gate head c9a711c259ed merged into trunk 827b89dbf109 — pass: builds betterbird, sent nowhere; no bus step; 2 wait(s) for a person; every machin…
mesh/repo-check head c9a711c259ed merged into trunk 827b89dbf109 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
writeAccountLists checked only for a running Betterbird and wrote without a sync;
it now waits for the profile's lock too and writes atomically. readLayer read any
answer that was not JSON as an empty layer, and a proposal replaces the layer
whole, so a changed wording would have dropped accounts; it now refuses. The
README says the rollout order that leaves no node on Betterbird's defaults.
2026-10-13 02:12:06 +02:00
jochen 1835f0efad betterbird: own Betterbird's whole configuration as settings, shown without a credential (hq issue 586, ADR 0329)
mesh/merge-gate head 1835f0efad0f merged into trunk 39e7a06e6293 — pass: builds betterbird, sent nowhere; no bus step; 2 wait(s) for a person; every machin…
mesh/repo-check head 1835f0efad0f merged into trunk 39e7a06e6293 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
The module owned only the accounts' server fields, so identities, signatures,
calendars, address books, filters and tags could differ between desktop nodes
unseen, and the operator's own preferences were shipped in the catalogue.

prefs becomes one entry per preference (a node's layer overrides one key) and the
defaults file keeps no value of the operator's; identities, calendars,
address-books, filters and tags become settings, adopted like accounts, filters
owned whole per account, and what Betterbird rewrites written only while it is
closed. New tools list each kind, derive the layers from every desktop node and
propose one at a time, and show drift; every answer passes a credential check.
2026-10-13 02:03:04 +02:00
mesh-admin 39e7a06e62 Merge pull request 'lab: runs name the one genesis bundle (issue 146)' (#261) from fix/146-lab-names-the-one-bundle into main 2026-10-12 23:22:05 +00:00
jochen 84911b94f3 Give the lab's runs the one genesis bundle (hq issue 146, ADR 0331)
mesh/merge-gate head 84911b94f3be merged into trunk 8f80cd471634 — pass: builds lab → shanks; no bus step; every machine composes with the change as it did…
mesh/repo-check head 84911b94f3be merged into trunk 8f80cd471634 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh-host now carries one genesis bundle, foundation-first-node.lock; the -nats one the
lab's runs named is deleted, so every run would have raised from a file that is gone.
2026-10-13 01:18:08 +02:00
jschoubben 1906081937 build-agent: declare busy, so a replaced agent finishes the check it runs
mesh/delivery-group group feat/335-a-busy-container-is-replaced-when-idle rejected: its order contradicts itself: novox/mesh-catalog@190608193764, novox/mesh-c…
mesh/merge-gate out of date: judged against trunk 8f80cd471634, the trunk is now 39e7a06e6293; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 8f80cd471634, the trunk is now 39e7a06e6293; judged again on a recheck or when it is merged
mesh/delivery stopped: the pull request closed unmerged
Every merge into mesh-controller replaced the agents and the queue started
their checks again elsewhere (issue 594). Its server now declares busy as
mesh-builder busy (novox/hq ADR 0335 §5).
2026-10-13 00:53:40 +02:00
jochen 4193eacec4 Mark grafana's and home-assistant's soft requirements optional (hq issue 166, ADR 0330)
mesh/merge-gate head 4193eacec459 merged into trunk 39e7a06e6293 — pass: builds grafana, home-assistant → ace; no bus step; every machine composes with the…
mesh/repo-check head 4193eacec459 merged into trunk 39e7a06e6293 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
grafana runs without influxdb and home-assistant without the Servarr apps or a broker, yet a mesh
without them could not assign either. Their requirements are now optional: bound when a provider
runs, absent otherwise, with nothing installed for them. grafana's data source and its credential are
delivered only while influxdb-api is bound, into directories it mounts, so an absent file never
becomes a directory docker made in its place.
2026-10-13 00:41:30 +02:00
mesh-admin 8f80cd4716 Merge pull request 'Name the setting keys searxng, keycloak and betterbird files take (hq issue 168, ADR 0326)' (#252) from fix/168-files-name-the-settings-they-take into main 2026-10-12 22:37:05 +00:00
mesh-admin 43a9155d5a Merge pull request 'claude-code: a session asks the operator on Telegram and reads the answer by a key (issue 589, hq ADR 0332)' (#258) from fix/589-a-session-asks-the-operator into main 2026-10-12 21:30:22 +00:00
jochen c4eebce598 Keep a question's slot until it expires while the router has not recorded it (issue 589, review 107)
mesh/merge-gate head c4eebce598cb merged into trunk 9c9a76d220f8 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head c4eebce598cb merged into trunk 9c9a76d220f8 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Freeing the slot two minutes after asking could let a backlogged router still show
the question after the session read it as ended, and let a third question in. The
seat's queue offers work again after an error and the router drops a cancel of an
ask it has not recorded, so a cancel cannot take it back; once expired, the router
refuses it if taken late. The slot test now uses a stale revision, so a plain
write in place of the compare-and-set fails it.
2026-10-12 23:24:41 +02:00
jochen 14999f1d3e Keep no secret-shaped question, and hold the two-question cap across nodes (issue 589, review 105)
mesh/merge-gate head 14999f1d3ecb merged into trunk 9c9a76d220f8 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head 14999f1d3ecb merged into trunk 9c9a76d220f8 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
A question was kept on the bus before the router judged its words, so a token in
it reached a bucket every node reads: every word, the options' too, is now held
to the secret shapes and invisible characters before anything is kept, and a
refused question keeps none of its words. The cap was check-then-create and
failed open: each open question now holds one of two slot keys taken by create
or compare-and-set, refusing when a slot cannot be read. A question the router
never recorded frees its slot after two minutes instead of at expiry. by is
required; wording matches ADR 0332.
2026-10-12 23:15:05 +02:00
jochen e4e0ce7619 Say the router's refusal of a proposal only once its discard is kept (issue 589)
mesh/merge-gate head e4e0ce761901 merged into trunk 9c9a76d220f8 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head e4e0ce761901 merged into trunk 9c9a76d220f8 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
The refusal test failed on a loaded build agent: Propose returned on hearing the
refusal before HeardEnd had written the discard, so a reader right after saw
nothing decided. The question path keeps the same order.
2026-10-12 22:51:21 +02:00
jochen 9446bec2f9 Let an agent session ask the operator on Telegram and read the answer (issue 589, hq ADR 0332)
mesh/merge-gate head 9446bec2f969 merged into trunk 9c9a76d220f8 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head 9446bec2f969 merged into trunk 9c9a76d220f8 — fail: its merge-check.sh failed: --- FAIL: TestTheRoutersRefusalIsSaidAndDiscards (0.08s)
mesh/delivery superseded: a newer head of the same pull request
A session that needs a decision could only ask in a terminal the operator may
not watch. claude_code_ask raises the question under this module's name at the
level approve, so only a channel that proves who answered carries it;
claude_code_answer reads the chosen option back by its key from the router's own
record, held to the question kept in the new questions state.
2026-10-12 22:44:45 +02:00
mesh-admin 9c9a76d220 Merge pull request 'Say how the build agent and the route proxy are ready (hq issue 544)' (#257) from fix/544-the-build-agent-says-it-is-ready into main 2026-10-12 20:00:06 +00:00
mesh-admin ac2d4dc46f Merge pull request 'mesh-issues: a fix's time from the move to fixing to delivered, and loud over its budget (issue 403, hq ADR 0313)' (#250) from fix/403-fix-time into main 2026-10-12 19:41:32 +00:00
jschoubben d09dbe33ac Say how the build agent and the route proxy are ready (novox/hq issue 544)
mesh/merge-gate head d09dbe33ac0f merged into trunk ac2d4dc46f4c — pass: builds build-agent, route-proxy → ace, g14, novox, shanks; no bus step; every mach…
mesh/repo-check head d09dbe33ac0f merged into trunk ac2d4dc46f4c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/delivery-group group fix/544-the-build-agent-says-it-is-ready delivered: every member is delivered
Neither server declared health, so each walk that moved them waited out the
60 s grace on every machine. The build agent is ready when 'mesh-builder ready'
says it took its build work; the route proxy when its http port accepts.
2026-10-12 21:33:13 +02:00
jochen 005f802504 Merge remote-tracking branch 'origin/main' into fix/403-fix-time
mesh/merge-gate head 005f802504fc merged into trunk eb5f33f2b475 — pass: builds mesh-delivery, mesh-issues → novox; no bus step; every machine composes wit…
mesh/repo-check head 005f802504fc merged into trunk eb5f33f2b475 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1791834103351744736 (novox/mesh-catalog at ac2d4dc4 into main,…
mesh/delivery-group group fix/403-fix-time delivered: every member is delivered
# Conflicts:
#	modules/mesh-delivery/cmd/mesh-delivery/model.go
2026-10-12 21:28:55 +02:00
mesh-admin eb5f33f2b4 Merge pull request 'betterbird: ship the operator's approved configuration, and show and fix drift from it (hq issue 345)' (#256) from fix/345-betterbird-configuration into main 2026-10-12 19:22:14 +00:00
jochen c4b4bac757 betterbird: never report the theme as fixed by clearing prefs.js, and never manage an account's name (hq issue 345, review 96)
mesh/merge-gate head c4b4bac757c5 merged into trunk 0863ec2a059d — pass: builds betterbird, sent nowhere; no bus step; 1 wait(s) for a person; every machin…
mesh/repo-check head c4b4bac757c5 merged into trunk 0863ec2a059d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Betterbird keeps the active theme in its add-on database, and extensions.activeThemeID only mirrors it.
So clearing the preference changed nothing, and the drift report then hid that. The theme is now read
from extensions.json against default-theme@mozilla.org. Another theme is drift, and its remedy is the
add-on manager: config_apply never clears it and keep refuses it.

A credential's, an account's or a server's name in the defaults file is refused by code: it is never
answered or cleared, and the check names it. Apply also refuses while the profile's lock is there, and
syncs prefs.js before the rename. A saved default of a built-in preference is no longer drift.
2026-10-12 21:09:25 +02:00
mesh-admin 0863ec2a05 Merge pull request 'A merged pull request is announced with its branch, so its delivery group is found (hq issue 578)' (#255) from fix/578-a-group-waits-for-its-controller-member into main 2026-10-12 19:07:03 +00:00
jochen 854c85393c betterbird: ship the operator's approved configuration, and show and fix drift from it (hq issue 345)
mesh/merge-gate out of date: judged against trunk 2fe5601397ae, the trunk is now 0863ec2a059d; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 2fe5601397ae, the trunk is now 0863ec2a059d; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
A module that installs Betterbird but leaves every node to its own settings does not set Betterbird up.
The operator approved the configuration on 2026-10-12, and it now ships in three layers. The policy
locks what must never differ: telemetry, add-on updates, the vendor's notices and the start page. A
defaults file carries the laptop's setup, so a fresh node gets it untouched. The setting prefs stays a
node's exceptions.

A default loses to a value the profile saved. betterbird_config shows that drift, betterbird_check
reports it, and betterbird_config_apply clears it while Betterbird is quit. keep proposes a node layer
instead of writing user.js. betterbird_prefs now reads Betterbird's real defaults from omni.ja, and
withholds of mailnews.* only what names an account. The default-client ask is decided from its own
policy entry, not from the first thing the policy lacks.
2026-10-12 20:58:39 +02:00
mesh-admin 2fe5601397 Merge pull request 'gitea: announce every open pull request's new head, not only the newest 20 (hq issue 579)' (#254) from fix/579-every-open-pull-request-is-announced into main 2026-10-12 18:58:27 +00:00
jschoubben 01d2ff7a46 Announce a merged pull request's branch, not its pull ref, so its delivery group is found (hq issue 578)
mesh/merge-gate head 01d2ff7a46bc merged into trunk 2fe5601397ae — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 01d2ff7a46bc merged into trunk 2fe5601397ae — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The merge tool deletes the branch after merging; the forge then reads head.ref as refs/pull/<n>/head and only
head.label names the branch. pull.merged carried the pull ref, so the controller found no group for the merge,
cut mesh-catalog #251's walk at once and walked it before mesh-controller #271's, against the group's order.
2026-10-12 20:57:14 +02:00
jschoubben 961e57f7e4 Page the announcer through every open pull request updated since its last look
mesh/merge-gate head 961e57f7e403 merged into trunk 22071dabfc7b — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 961e57f7e403 merged into trunk 22071dabfc7b — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
A pass read only the 20 most recently updated open pull requests of a
repository, so a new head beyond them went unannounced and its merge check
never started (novox/hq issue 579). Pages stop at the floor; a sweep reads all.
2026-10-12 20:52:27 +02:00
mesh-admin 22071dabfc Merge pull request 'betterbird: lock the ask to be the default mail client off (hq issue 345)' (#253) from fix/345-no-default-client-check into main 2026-10-12 18:23:49 +00:00
jochen 481e44cdaa betterbird: lock the ask to be the default mail client off, so it never shows at a start (hq issue 345)
mesh/merge-gate head 481e44cdaa29 merged into trunk 23ea0f431faf — pass: builds betterbird, sent nowhere; no bus step; 1 wait(s) for a person; every machin…
mesh/repo-check head 481e44cdaa29 merged into trunk 23ea0f431faf — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Betterbird's own default asks at every start with a window to become the default for mail, news, feeds and
calendar files. The policy locks mail.shell.checkDefaultClient off, which wins over a profile's own value, and
betterbird_check says when a node would still ask.
2026-10-12 20:17:38 +02:00
mesh-admin 23ea0f431f Merge pull request 'A verdict goes out of date when its trunk moves, and the merge tool refuses it (hq ADR 0323, issue 341)' (#251) from feat/341-a-merge-check-judges-the-merge into main 2026-10-12 16:11:28 +00:00
jschoubben 41b6e10077 Send the announced base head with a check mesh-delivery asks
mesh/merge-gate pass: builds gitea, 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
mesh/delivery-group group feat/341-a-merge-check-judges-the-merge delivered: every member is delivered
The controller names it as the trunk commit of a verdict no build judges; a
recheck without it was answered with none (novox/hq ADR 0323).
2026-10-12 17:52:20 +02:00
jochen 2f55adc57c Name the setting keys searxng, keycloak and betterbird files take (hq issue 168, ADR 0326)
mesh/merge-gate head 2f55adc57c60 merged into trunk 43a9155d5afe — pass: builds betterbird, keycloak, searxng → ace; no bus step; 3 wait(s) for a person; e…
mesh/repo-check head 2f55adc57c60 merged into trunk 43a9155d5afe — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Every setting reached every mergeable file: searxng's endpoints landed in
its settings.yml, keycloak's issuer in its tools' config, betterbird's
prefs would land in its accounts file. Each file now names the keys it
reads, so the controller lays only those over it and calls the rest
stray. Merge only after the controller that knows takes-settings
(mesh-controller #272) has walked: an older one hands the field to the
node-engine, which refuses it.
2026-10-12 17:47:27 +02:00
jschoubben 434506f9b1 Serve the merge tool beside the one writer, and close the review's gaps
mesh/delivery-group group feat/341-a-merge-check-judges-the-merge rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds gitea, 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)
The merge tool's pending bypassed the merge check's one writer from the tools'
bundle (issue 456); it is now served beside it. A verdict no build judges names
the announced base head, so the merge tool no longer refuses it for ever. A
late verdict keeps its account, a trunk is read again in each head's turn so a
fresh verdict is not put out of date, and a delivered member counts when a
rebased member's group is composed again (novox/hq ADR 0323).
2026-10-12 17:46:39 +02:00
jschoubben 82cca650a2 Put a verdict out of date when its trunk moves, and refuse a merge on one
mesh/delivery-group group feat/341-a-merge-check-judges-the-merge checking: 0 of 3 member(s) ready
mesh/merge-gate pass: builds gitea, 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 superseded: a newer head of the same pull request
A merge check's verdict now names the trunk commit it judged (novox/hq ADR
0323, issue 341), but the forge kept it green after another merge moved the
trunk, the merge tool merged on it, and mesh-delivery kept the delivery ready.
The forge's holder puts both statuses of every open pull request into a moved
trunk back to pending with the reason and runs nothing; the merge tool compares
the verdict's trunk commit with the trunk's head, asks the check once and never
merges by itself; mesh-delivery sends a ready delivery back to proposed, counts
only a verdict for the trunk's head, and composes a group again for a rebased
member instead of holding it.
2026-10-12 17:29:58 +02:00
mesh-admin cd2b6aacd8 Merge pull request 'mesh-reads: a node's files and accounts read as an account that holds no secret (hq ADR 0310, issue 418)' (#247) from fix/418-node-reads-2 into main 2026-10-12 14:52:18 +00:00
jochen aed7e0c968 Read the mesh's comment verdicts as approvals, and warn on no fix timed from the log (issue 403, review 69)
mesh/delivery-group group fix/403-fix-time checking: composed check out of date: novox/mesh-controller@7ca5a45ecc5a was judged against trunk ?, the trunk is no…
mesh/merge-gate out of date: judged against trunk 2fe5601397ae, the trunk is now 0863ec2a059d; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 2fe5601397ae, the trunk is now 0863ec2a059d; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
The forge refuses an approval of one's own pull request, so the independent
reviews are comment reviews opening with "Verdict: approve at <sha>"; read
only APPROVED, the review phase was never measured. A fix whose start was
read back from the log was fixed before fix times were measured: it is kept
and shown, never warned on. A carried value at fixing after one at diagnosing
starts at its worker's active (the move), not its since (the diagnosis).
2026-10-12 16:16:04 +02:00
jochen 43df7ebc5a Measure each fix from the move to fixing to delivered, and say one over its budget (issue 403)
mesh/merge-gate pass: builds mesh-delivery, mesh-issues → 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-group group fix/403-fix-time ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
hq ADR 0313: the operator's five minutes start when an agent moves the issue
to fixing, not at the merge. mesh-delivery says on each transition, and in
checks, when the pull request opened, when it merged and the class of what it
moves. mesh-issues keeps the first move to fixing, the fix time with its four
phases once the last fix is delivered, serves fix-times, and says
issue-tracker.fix.<class>.over-budget on the operator's channel, cleared by the
next fix of that class within budget.
2026-10-12 15:50:12 +02:00
mesh-admin c5e1bc7bb4 Merge pull request 'mesh-issues: a resolved issue ends its fix-stuck warning, and a closed issue's fix is never said stuck (hq issue 547)' (#239) from fix/547-resolved-ends-stuck into main 2026-10-12 13:21:49 +00:00
mesh-admin 6190b40175 Merge pull request 'artifact-store-tools: an image a definition pins, under any name the store has had, is named by a definition (hq issue 323)' (#249) from fix/323-named-by-a-definition-2 into main 2026-10-12 13:08:31 +00:00
mesh-admin edf39a3b74 Merge pull request 'betterbird: Betterbird from its pinned release, Owl by policy, accounts declared without credentials and adopted, and a reminder test that is visibly a test (hq issue 345, ADR 0316, ADR 0317)' (#240) from fix/345-betterbird-module into main 2026-10-12 12:54:53 +00:00
jochen 15983e2f96 betterbird: an account's own aliases and identities that do not send by SMTP share no outgoing server, and the pins name their vendors on purpose (review 36, review 35 of mesh-controller #251)
mesh/merge-gate pass: builds betterbird, sent nowhere; no bus step; 1 wait(s) for a person; every machine composes with the change as it did without (4 of …
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 failed: its walk failed, carrying its own commit: a gate on a first machine (what it carried put back), a build, a machine
The shared-server check refused an adopted account for its own alias, and
counted Owl's, local folders' and feeds' identities as users of the default
server, though they never send by SMTP. The controller now judges an
upstream pin's host like any name, so each pin declares its vendor.
2026-10-12 14:49:21 +02:00
jochen 263c86d2e5 betterbird: withhold account values in the user.js answer, never write a shared outgoing server, and say a newer Owl as a fact (review 34, hq issue 345, ADR 0317)
The user.js answer carried declared addresses and hosts (ADR 0316 §16). An
adopted identity's outgoing server may be shared with an account the module
does not own, whose sending the declared values would change at every start:
refused with the remedy, since ADR 0317 §2 leaves the identity's server and
the lists as they are; a recorded adoption follows the identity's current
server. Owl installed by policy cannot be removed, so a newer Owl than the
pin is a fact whose remedy is moving the pin.
2026-10-12 14:49:21 +02:00
jochen ec4ab7afe7 betterbird: install Betterbird from its pinned release, ship Owl by policy, and declare the accounts without credentials (hq issue 345, ADR 0316, ADR 0317)
A module that cannot set Betterbird up is of little use (the operator,
2026-10-12). The module now declares the community package absent and
unpacks the pinned upstream release where that package had it, so the same
profile opens; places the enterprise policy that turns Betterbird's updater
off and force-installs Owl from the xpi the mesh pinned by its vendor's
digest (never in a repository); and takes the operator's accounts from the
trusted setting accounts, without any password or token: an account the
profile already holds is adopted under its own keys, a new one is created
under the module's mesh_ keys, an ambiguous one is refused with the remedy,
and the account lists are only written into. betterbird_accounts says the
profile's accounts by count and proposes the setting built from them for the
operator's Approve. Every new build waits for the operator's push. The
reminder test is visibly a test (MESH TEST band, event name) and its
description says it opens a window on the operator's screen.
2026-10-12 14:49:21 +02:00
jochen 0e6f382bb7 betterbird: keep the reminder test's update check off so no About window takes the screen (hq issue 345)
A fresh profile has never checked for updates, so Betterbird checked at the
test's start, found a newer build published and opened its About window on the
operator's screen instead of leaving the reminder to show. The test's own
preferences now also come last in its user.js, so nothing declared or carried
overrides them.
2026-10-12 14:49:21 +02:00
jochen d745a4d31e artifact-store-tools: say an image a definition pins is named by a definition, under any name the store has had (issue 323)
mesh/merge-gate pass: builds artifact-store-tools → 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
No record names an image the mesh did not make, so one a definition pins under an
older name of the store read as unrecorded, the same word as an image nothing
uses. The tool now reads every definition through the catalogue and says
named-by-a-definition with the module. The store's older names are the mesh's
setting older-store-names, since a catalogue names no installation.
2026-10-12 14:47:06 +02:00
jochen 994cab533b mesh-reads: read a node's files and accounts as an account that holds no secret (hq ADR 0310, issue 418)
mesh/merge-gate pass: builds new: modules/mesh-reads, 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
Reading a file, searching one, listing a directory, the owner and mode of each
component of a path and looking up an account went through the login shell's
execute as the operator's account, which sees the operator's keys and the
agent's credentials. This module holds node-reads on every node and runs as
mesh-reads (no login, no group beside its own). It refuses the node's secret
places by name, judging the file actually opened rather than the name asked,
so a flipped link reaches nothing refused; withholds any line shaped like a
secret through mesh-sdk's outward rule and every line of a BEGIN/END block;
and bounds every answer. Replaces mesh-catalog #244 (reviews 55), whose first
commit carried a built binary.
2026-10-12 14:32:22 +02:00
mesh-admin 2772120758 Merge pull request 'mesh-delivery: say how many owed effects were tried since the start (hq issue 407)' (#246) from fix/407-owed-effects-are-counted into main 2026-10-12 12:23:36 +00:00
mesh-admin 13a7d40ac5 Merge pull request 'mesh-knowledge: a record may change a drafted clause, and a record whose clauses others link by number is never redrafted (hq issue 494, ADR 0321)' (#245) from fix/494-change-a-drafted-clause into main 2026-10-12 12:18:34 +00:00
jschoubben a3ad3c5061 Count the owed effects mesh-delivery tried, so a window's writes can be judged against them
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
delivery_status said only what is owed now; issue 326's rule needs the tries made in a window.
It now says the tries since this owner started, by how each ended, and the last try's time.
(novox/hq issue 407)
2026-10-12 14:16:58 +02:00
mesh-admin 5b75499ea6 Merge pull request 'mesh-delivery: say an unanswered line's delivery state as data (hq issue 451)' (#243) from fix/451-a-recheck-is-the-step into main 2026-10-12 12:14:45 +00:00
mesh-admin 0366208274 Merge pull request 'nats: say whether a user's grant is in force on the running server (review of hq issue 353)' (#166) from feat/353-the-bus-says-a-grant-is-in-force into main 2026-10-12 12:13:25 +00:00
jochen 9f893b6305 mesh-knowledge: let a record change a drafted clause, and never redraft a record whose clauses others link by number
mesh/merge-gate pass: builds mesh-knowledge → 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 changes link onto a drafted clause was refused, so records that really
change such a rule could only cite it (ADR 0294, 0301, 0320); and return
renumbered ADR 0281's clauses, pointing other records' links at other
clauses. The changed drafted clause keeps its number and stays unserved;
return refuses a record whose clauses another record changed or links by
number. Issue 494, ADR 0321.
2026-10-12 14:05:37 +02:00
mesh-admin d1baca9b94 Merge pull request 'mesh-delivery: a stop a later merge covered reads covered, and stays in deliveries while stalled lists it (hq issue 480)' (#242) from fix/480-a-covered-stop-reads-covered into main 2026-10-12 11:56:09 +00:00
mesh-admin 3a5c86b5d6 Merge pull request 'messenger: a replaced ask's own message ends with it, and a cleared record owes nothing (hq issue 548)' (#241) from fix/548-replaced-ask-message into main 2026-10-12 11:50:29 +00:00
jschoubben befe451f6d mesh-delivery: say an unanswered line's delivery state as data (hq issue 451)
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
mesh/delivery-group group fix/451-a-recheck-is-the-step delivered: every member is delivered
The controller advises a recheck for a ready or rejected delivery whose
check answered once, and must read that state from the line, not its words.
2026-10-12 13:47:52 +02:00
jschoubben 7ad54356fd Say in the deliveries verb's description that a stop stalled lists stays listed
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
2026-10-12 13:46:55 +02:00
jschoubben 69f76a53e7 Read a stop a later merge covered as covered, and keep it in deliveries while stalled lists it
A stop for a walk that never built its modules (issue 474) kept saying "error: stopped" on the forge and in
show after a later delivered merge carried its change to every machine. The stop stays final, as a covered
superseded member stays superseded (issue 442); its status, view, note and show now say covered and by which
merge, told once and from what is kept, so a restart or a stop covered before this tells it too. deliveries
dropped such a stop after a day while stalled still listed it; it now lists it as long as stalled does.

novox/hq issue 480, points 1 and 3.
2026-10-12 13:46:23 +02:00
mesh-admin b82aa1691c Merge pull request 'mesh-knowledge: the review asks changes for a decision nothing would cite, so its acceptance never turns the module unhealthy (hq issue 504)' (#237) from fix/504-propose-refuses-uncited-record into main 2026-10-12 11:44:58 +00:00
jochen 7e27259a64 messenger: end a replaced ask's own message, and drop what a cleared record owed (issue 548)
mesh/merge-gate pass: builds messenger → 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 replaced ask's message was left open as if the newer ask took it over, but
the newer ask opens a message of its own, so the old one kept offering answers
nobody takes. It now ends on every channel with 'Replaced by a newer question',
and the minute's sweep ends any such message left from before. A record that
clears no longer keeps what a walk owed of it (the review of PR 238).
2026-10-12 13:44:08 +02:00
mesh-admin 3fe19c9c0e Merge pull request 'messenger: a walk's held messages are told only once its message was sent, handed on only as a first hold would take them, and its start and end compared on the controller's clock (hq issue 538)' (#238) from fix/538-walk-hold-told-handover-clock into main 2026-10-12 11:41:47 +00:00
jochen 424fc5a281 messenger: keep what a walk held owed in the state until its message was sent, so a restart says a held warning too (issue 538, review 39)
mesh/merge-gate pass: builds messenger → 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 warning held on purpose and lost with the walk's message at a restart was turned
into history and never told. Name the walk in the told line from its own item.
2026-10-12 13:35:48 +02:00
jochen f067fab9db mesh-issues: end a fix-stuck warning when its issue is resolved, and never say a closed issue's fix stuck (issue 547)
mesh/merge-gate pass: builds mesh-issues → 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 resolved issue's worker proved the acceptance live, so a fix whose own walk failed
and that a later merge carried is not waited for. Only wontfix ended the warning, so
16 warnings stayed open on the operator's channel and the sweep raised new ones for
resolved issues.
2026-10-12 13:31:51 +02:00
jochen 56a9a1b082 mesh-knowledge README: say a failed whole-set rule is a warning to the operator and leaves the module healthy (hq issue 504)
mesh/merge-gate pass: builds mesh-knowledge → 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
2026-10-12 13:27:06 +02:00
jochen df94be0e60 messenger: mark a walk's held messages told only once its message was sent, hand on only what a first hold would take, compare a walk's start and end on the controller's clock (issue 538)
mesh/merge-gate pass: builds messenger → 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 superseded: a newer head of the same pull request
A message lost with a restart before it was sent left what it carried marked told,
so it reached the operator only as its reminder hours later. A later walk took over
silenced, already said or asked messages that ADR 0308 section 3 never holds. A start
capped at the router's now was compared with the controller's moments.
2026-10-12 13:26:41 +02:00
jochen 8f5e8bcb6e mesh-knowledge: tell a failed whole-set rule to the operator as a warning naming the records, and leave the module healthy (hq issue 504)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds mesh-knowledge → 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)
The operator decided on 2026-10-12 that a rule of the record set that fails
is a fault of the records, which their authors repair, not the module
failing: as issue 508 decided for a proposal never reviewed, health says
only whether the module works, and a finding must not hold the module's
own rollouts. The findings go through the notice the knowledge base
already sends (operator-channel.notify, a warning), once per finding,
kept as told across restarts, and cleared when the rule holds again.
2026-10-12 13:25:09 +02:00
jochen ca3c3e6dfd mesh-knowledge tests: set the local zone once before any goroutine, not under a running bus server (race found by the repository check on #237)
inZone wrote time.Local while the bus test's nats server read it from a
goroutine that outlives its test; the race detector the repository check
now runs failed TestAReviewOwedOutlivesARestartAndItsLastTryIsSaid.
2026-10-12 13:25:09 +02:00
jochen 2051d8b80b mesh-knowledge: ask changes in the review for a decision nothing would cite, so its acceptance never turns the module unhealthy (hq issue 504)
The whole-set rule cited ran only after acceptance: the review approved a
decision with no home and no record linking it, the rule then failed, and
the module's health turned red for a fault its author could only repair
after the fact. hq held the rule at the record's own merge. The review now
judges it ahead, before the judgement: such a decision stays proposed with
a changes finding naming the rule. It is not refused at propose, because
no record can link a number only reserved.
2026-10-12 13:25:09 +02:00
mesh-admin 3e84581927 Merge pull request 'messenger: hold what a delivery walk touches, tell its end once, and say nothing again for a restart (hq ADR 0308, issues 532, 531)' (#230) from adr-0308-hold-during-walks into main 2026-10-12 11:21:16 +00:00
mesh-admin 4d9cb0827f Merge pull request 'gitea forge bundle: the parent the forge used, protection rules as the forge matches them, size and time bounds, one HTTP client (issue 539)' (#236) from fix/539-forge-commit-hardening into main 2026-10-12 10:58:07 +00:00
jochen ac90b35b49 guard.ts: cover every branch for a rule Go's regexp reads otherwise, and hold both matchers to one table (issue 539)
mesh/merge-gate pass: builds gitea → 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
Review 30 found guard.ts missing branches the forge protects: Go reads \d, \w,
\pL, a class opening with ] or ^, and [[:digit:]] otherwise than JavaScript.
A rule using any of them now covers every branch, so the guard refuses more
than the forge, never less. test/rule-cases.json holds the forge's answers,
read by the Go and TypeScript tests alike.
2026-10-12 12:51:22 +02:00
jochen f2d765320a gitea forge bundle: name the parent the forge used, match protection rules as the forge does, bound a commit's size and a search's time (issue 539)
The forge's contents and diffpatch endpoints take a branch, never a commit to
build on, so the parent is read from the commit the forge made, and a branch
that moved under the commit is said. Blobs are read at the commit read first,
not at the branch name. Protection rules match as Gitea 1.27 matches them
([...], {a,b}, escapes, a plain name in any case), in the Go bundle and in
guard.ts alike. One commit carries at most 4 MiB per file or patch and 16 MiB
together; code search stops at its deadline and says so. The HTTP client the
two Go bundles copied is one package, internal/forgehttp.
2026-10-12 12:51:22 +02:00
mesh-admin c5abdf35ed Merge pull request 'A module whose tests could not run fails the repository check (issue 385)' (#231) from fix/385-an-untestable-module-fails-the-check into main 2026-10-12 10:48:11 +00:00
mesh-admin 9d6bd2d475 Merge pull request 'gitea: a recheck's pending goes through the one writer of mesh/merge-gate (issue 456)' (#234) from fix/456-a-recheck-writes-through-the-one-writer into main 2026-10-12 10:32:00 +00:00
mesh-admin d06733c490 Merge pull request 'mesh-knowledge: the bus test passes on any day (issue 540)' (#235) from fix/540-knowledge-test-clock into main 2026-10-12 10:31:18 +00:00
jochen 7628b0e5b9 mesh-knowledge: give the bus test's bundle a fixed clock, so it passes on any day (issue 540)
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
The bundle dated a reservation from the real day while the proposal kept the
fixture's 2026-10-11, so the date rule refused it on every later day.
2026-10-12 12:23:38 +02:00
jschoubben 5053e96d4e Route a recheck's pending through the gitea module's one writer of mesh/merge-gate
mesh/merge-gate pass: builds gitea → 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
The recheck's pending was written from the tools' bundle, its own process, past the
writer's per-head chain: the first run's verdict mark stayed set, so a rechecked head
never said where its new run builds, and an old run's verdict in flight or redelivered
could land over the pending. The status tool is now served from the watcher's bundle
and writes the merge check's pending through the writer, and a verdict whose ask
(checked.id) predates the check now pending is dropped, weighed after a restart against
the forge's own history of the gate (novox/hq issue 456).
2026-10-12 12:20:23 +02:00
mesh-admin b707f27911 Merge pull request 'gitea: an agent's commit on a branch of its own, and the reads around it (issue 375)' (#233) from fix/375-a-gitea-commit-tool into main 2026-10-12 10:14:05 +00:00
jschoubben 4fdb047cc8 nats: say whether a user's grant is in force on the running server, not only written (hq issue 353)
mesh/merge-gate pass: builds nats, sent nowhere; merging this needs a person's bus upgrade; 1 wait(s) for a person; every machine composes with the change …
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
nats_user_can read the user list on the machine. A list written is not a list the bus enforces until
the server reloads it: on 2026-10-09 the controller asked between the write and the reload's effect.
The answer now carries in_force — allowed in the file, and the server's config_load_time after the
file's write — with why when it is not, and anything not known is not in force.
2026-10-12 12:13:03 +02:00
jochen a92c08fd5b messenger: keep what a walk held until it is said, and open no walk whose end came first (review of #230)
mesh/merge-gate pass: builds messenger → 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
- No channel ready at a walk's end or bound: what it held is left unsent and tried
  again every minute, as any other message, instead of lost with the walk.
- At a walk's end or bound, a message another open walk touches is held on by
  that walk, even one started after it was held (hq ADR 0308 §2).
- A walk read back past its forget is released at its bound first, then forgotten.
- A walk's end is kept a day: a start heard after it (same started_at, or not
  later than the end) opens nothing, also after the router restarts; a later
  start under the same id still opens.
- A walk's plain end, when its words cannot be said, leaves what it held to be
  said on its own instead of marking it told.
- The restart pass no longer skips a held message: one with an open ask is never
  held (ADR 0308 §3), so its answers are made to work again.
2026-10-12 12:07:38 +02:00
jochen 9eb2ea8d69 gitea: let an agent put a commit on a branch of its own, and read history, compares, reviews, directories and code (issue 375)
mesh/merge-gate pass: builds gitea → 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
An agent could not get a finished change onto the forge without someone
pushing by hand, so the branch / pull request / approval rule could not be
followed. A Go bundle serves gitea_commit, held to gitea_api's rule (never the
default branch, never a protected one), and the summarised reads agents
otherwise made through gitea_api as raw JSON.
2026-10-12 12:06:23 +02:00
jochen de8a866189 Fail the check when a module's tests could not run, and take a pinned SDK from its clone beside (issue 385)
mesh/delivery delivered
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh, merge-check-typescript.sh): main's version judged it; the change's judges the pull r…
mesh/delivery-group group fix/385-an-untestable-module-fails-the-check delivered: every member is delivered
A module whose dependencies could not be fetched was said NOT TESTED and the
repository check passed, so a merge on it would have landed untested code.
The build seat clones the SDK beside every check already: a forge requirement
the module cache lacks is now taken from that clone through a go.mod of its
own, and a module that still cannot be built fails the check by name and why.
A TypeScript module whose dependencies the toolchain lacks fails the same way.
2026-10-12 12:03:50 +02:00
jschoubben 991ffcfda0 messenger: run a walk's bound from its started_at, as the controller's walk events say it (hq ADR 0308 §5)
mesh/merge-gate pass: builds messenger → 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 superseded: a newer head of the same pull request
2026-10-12 11:56:30 +02:00
jschoubben 2927cb2ceb messenger: hold what a delivery walk touches and tell its end once, and say nothing again for a restart (hq ADR 0308, issues 532, 531)
A delivery's own restarts reached the operator as notifications while it ran, and every restart of the
tool runner popped the four silenced messages up again on the desk, twice. The router now reads the
controller's walk start and end on plan-moved, holds what an open walk touches until its end or bound,
tells the end as one message, and makes the restart pass once per restart from its own state, leaving
silenced, withheld, answered and held messages as they are.
2026-10-12 11:55:35 +02:00
mesh-admin da296cacc6 Merge pull request 'mesh-delivery: give a waiting walk its word or say what holds it (issues 529, 362)' (#229) from fix/529-a-waiting-walk-is-answered-or-named into main 2026-10-12 09:55:20 +00:00
jschoubben 1de89cd702 Name a walk of nothing-to-move merges instead of letting it go (issue 529)
mesh/delivery delivered
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-group group fix/529-a-waiting-walk-is-answered-or-named delivered: every member is delivered
The review showed the give-the-word rule skipped a failed or missing check
and group order, and let go moves nothing judged; to-be 47 has no such path.
Such a walk is now said, by stalled with walks, as held by nothing, why.
2026-10-12 11:33:51 +02:00
jschoubben 2bee70a478 Give a waiting walk its word or say what holds it (issues 529, 362)
mesh/delivery superseded: a newer head of the same pull request
mesh/delivery-group group fix/529-a-waiting-walk-is-answered-or-named checking: 0 of 2 member(s) ready
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)
A merge whose plan sends nothing is delivered at once, yet the controller
can still open a walk for it; no live delivery then held the walk and it
waited for a person (the nzbgeek case). Give such a walk its word.

Name each held delivery in the status, and let stalled say, for every
waiting walk, what holds it, so the controller's plans line points at the
person's decision instead of a hand start.
2026-10-12 11:24:30 +02:00
mesh-admin 6546c05f6e Merge pull request 'mesh-issues: the issues are kept in the log changes, and the bucket is their fold (hq ADR 0300, issues 501, 502)' (#228) from adr-0300-issues-on-a-log into main 2026-10-12 08:46:10 +00:00
jochen d413da203c mesh-issues: resume an interrupted carry by revision and fault one issue instead of the whole start, so no kept value is lost and the tracker always starts (review of PR 228)
mesh/delivery delivered
mesh/merge-gate pass: builds mesh-issues → 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)
An open's lost answer is judged by the issue's first entry; an entry that does not
fold faults its issue on start; a value written beside the log is never folded over;
one issue is kept in at most 252 KiB so its whole operation fits a log entry.
2026-10-12 10:35:57 +02:00
jochen d33f3e3f71 mesh-issues: pin mesh-sdk at main 9747f3e, where the log and the history are both merged
mesh/merge-gate pass: builds mesh-issues → 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 superseded: a newer head of the same pull request
2026-10-12 10:22:19 +02:00
jochen dcc7510cd8 mesh-issues: carry each value the issues bucket kept, read through the runtime's history, so the log begins with all the bucket still knew (hq ADR 0300 §6, ADR 0307)
mesh/merge-gate pass: builds mesh-issues → 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 superseded: a newer head of the same pull request
2026-10-12 10:20:51 +02:00
jochen 39da2cd793 mesh-issues: keep the issues bucket at 8 values until the carry can read them, so the bus drops nothing before it is carried (hq ADR 0300 §6)
mesh/merge-gate fail: builds mesh-issues → novox; no bus step; a manifest the change touches fails the module check: modules/mesh-issues/module.json: this …
mesh/repo-check fail: its merge-check.sh failed: files asking for a setting without saying whether it is trusted: 25
mesh/delivery superseded: a newer head of the same pull request
2026-10-12 02:43:58 +02:00
jochen 983d434623 mesh-issues: keep every issue in the log changes and the bucket as its fold, so the record outlives the bucket's cap and a week of events (hq ADR 0300, issues 501, 502)
mesh/merge-gate fail: builds mesh-issues → novox; no bus step; a manifest the change touches fails the module check: modules/mesh-issues/module.json: this …
mesh/repo-check fail: its merge-check.sh failed: files asking for a setting without saying whether it is trusted: 25
mesh/delivery superseded: a newer head of the same pull request
Every write appends one operation at the issue's last sequence before the
bucket is written; a refused append writes nothing. The start carries what
the bucket holds, refolds what is behind and indexes every issue in memory,
so list answers from memory with a ranked search and a before cursor.
2026-10-12 02:41:54 +02:00
mesh-admin 8a92a47bf7 Merge pull request 'Hold a leaf delivery to three minutes in times (hq ADR 0304 decision 4, issue 525)' (#227) from feat/525-leaf-budget-three-minutes into main 2026-10-12 00:02:25 +00:00
jschoubben 2711e812c8 Hold a leaf delivery to three minutes in times (hq ADR 0304 decision 4, issue 525)
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
mesh/delivery-group group feat/525-leaf-budget-three-minutes delivered: every member is delivered
ADR 0304 lowered a leaf module's delivery budget from five minutes to
three; times says each class against the controller's same budget.
2026-10-12 01:45:14 +02:00
mesh-admin a0fb555f79 Merge pull request 'Read the tunnel through sudo where the tool runner is not root (hq issue 522)' (#226) from fix/522-tunnel-escalates into main 2026-10-11 22:07:54 +00:00
jschoubben b1280f81a2 Read the tunnel through sudo where the tool runner is not root (hq issue 522)
mesh/merge-gate pass: builds netcheck → 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
On novox the tool runner runs as the operator's account, and wg show
answered 'Operation not permitted'. Root is the module's concern: the
command runs as given when the process is root, else through sudo -n,
which the sudo module allows the runtime's account, as nftables does.
2026-10-12 00:05:04 +02:00
mesh-admin d2dff99bcf Merge pull request 'Show the tunnel's peers as each machine sees them, and say on the bus when they move (hq issue 522)' (#225) from feat/522-tunnel-peers into main 2026-10-11 21:55:14 +00:00
jschoubben c31e29f9a6 Keep watching the tunnel after a failed look, and remember peers' names
mesh/merge-gate pass: builds netcheck → ace, g14; 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
2026-10-11 23:51:40 +02:00
jschoubben 4fc769ed29 Show the tunnel's peers as each machine sees them, and say on the bus when they move (hq issue 522)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds netcheck → ace, g14; 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)
ace's tunnel leg lost most packets while its own check called the tunnel
healthy, and nothing on the mesh could say which address and port the
hub saw for each peer, which is what tells a home router's port mix-up
from anything else. netcheck_tunnel answers each peer's endpoint, last
handshake and traffic, never a key, and netcheck now says when a
peer's endpoint changes or its handshake goes stale or fresh again.
2026-10-11 23:50:17 +02:00
mesh-admin 11be8a548a Merge pull request 'mesh-delivery: a stop ends only its own change on a shared walk (hq issue 459, ADR 0299)' (#221) from feat/459-a-stop-re-walks-the-others into main 2026-10-11 19:49:16 +00:00
mesh-admin f7fdb5e87b Merge pull request 'restic: nothing declared has no overdue night; a count's age is judged by awake time (issue 489)' (#224) from fix/489-nothing-declared-is-healthy into main 2026-10-11 19:31:05 +00:00
jschoubben 7f05fad58a restic: judge no night overdue where nothing is declared, and a count's age by the time the machine was awake
mesh/merge-gate pass: builds restic → 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
A machine that backs up nothing read as missing its night after 26 hours up, and a laptop's count, last
written before it slept, read as stale on waking until the next look (novox/hq issue 489).
2026-10-11 21:10:11 +02:00
mesh-admin fca2f92216 Merge pull request 'mesh-issues: reserve's description says it asks the knowledge base (issue 493)' (#223) from fix/493-reserve-says-what-it-reads into main 2026-10-11 19:07:27 +00:00
jschoubben c0710e2db7 Say that reserve asks the knowledge base for the highest number, not hq (hq issue 493)
mesh/merge-gate pass: builds mesh-issues → 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
Since #214 reserve reads knowledge_highest; its description still named hq's folders.
2026-10-11 20:53:21 +02:00
mesh-admin 3fa076829f Merge pull request 'mesh-knowledge, claude-code: a long record is reviewed within a bound sized to it, and a proposal waiting on its author leaves the module healthy (hq issue 508)' (#222) from fix/508-a-long-record-is-reviewed into main 2026-10-11 18:41:53 +00:00
jschoubben fcab25604e Review a long record within a bound sized to it, and let a proposal waiting on its author leave the knowledge base healthy (hq issue 508)
mesh/merge-gate pass: builds claude-code, mesh-knowledge → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (…
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 judgement over one bus call's 30 seconds is collected by its ticket from
claude-code, so an eleven-clause record can be reviewed; a proposal out of
review tries is told to the operator, not raised as the module's fault, so
it no longer holds the module's own rollouts.
2026-10-11 20:32:33 +02:00
jschoubben 3d7f9a3e66 mesh-delivery: say a shared-module leave-out truly, hold back only what was stopped by an act, and say a held walk where it is read (review of #221)
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
mesh/delivery-group group feat/459-a-stop-re-walks-the-others delivered: every member is delivered
A merge left out for sharing a module read as containing the stopped merge; the
safety net also held back merges stranded by their walk, which nobody stopped;
a held walk said why only in the log; and a stop of a merge in a batch did not
ask the batch.
2026-10-11 20:27:55 +02:00
jschoubben 8d7f60c9e2 mesh-delivery: never ship a stopped merge walked again, and stop only once the walk is (review of #221, hq issue 459)
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-group group feat/459-a-stop-re-walks-the-others checking: 2 of 3 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
A merge walked again into a batch and then stopped by its group stayed in that
batch, which was later let go. Any walk or batch that names a stopped
delivery's merge is now asked to leave it out and is never let go; a refused
leave-out stops the whole walk. A person's stop marks the delivery stopped only
after its walk is, and the answer says what stays on the machines and what
will still carry the change.
2026-10-11 20:16:37 +02:00
jschoubben aeab5b80b4 mesh-delivery: a stop ends only its own change on a shared walk (hq issue 459, ADR 0299)
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-group group feat/459-a-stop-re-walks-the-others checking: 2 of 3 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
A group stop ended the stopped member's walk, and with it every other delivery
the batch put on that walk. It now asks delivery-stop to leave the stopped
merges out, so the others are walked again; a person's stop on a shared walk
first says what ending the walk would take down and acts only on a way.
2026-10-11 19:59:43 +02:00
mesh-admin 977e7a6fd2 Merge pull request 'mesh-knowledge: a review owed says when it is due, survives a restart, and its last try is told to the operator (hq issue 497)' (#219) from fix/497-a-review-owed-outlives-a-restart into main 2026-10-11 17:49:34 +00:00
jschoubben c394ecd9fd mesh-knowledge: give a never-reviewed notice its resolved line when raised, and test a refused clear and local time in a set zone (review of #219)
mesh/merge-gate pass: builds mesh-knowledge → 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 covered after its failure: a later walk that contains it was delivered: plan-1791744131193016639 (novox/mesh-catalog at 3fa07682 into main,…
The operator's channel edits a cleared message with the resolved line it was raised with, so one sent only with
the clear was never shown.
2026-10-11 19:41:45 +02:00
jschoubben d45761678a mesh-knowledge: clear a never-reviewed notice once the record moves on, key it by version and run of tries, say times locally (review of #219)
mesh/merge-gate pass: builds mesh-knowledge → 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 superseded: a newer head of the same pull request
A notice left open kept telling the operator a record was proposed and not in force after it was proposed again
or accepted, and a later version's notice merged into it.
2026-10-11 19:36:14 +02:00
mesh-admin 9f63231db9 Merge pull request 'mesh-issues: take every condition key the controller raises and every delivery id mesh-delivery gives (hq issue 475)' (#218) from fix/475-a-delivery-condition-can-be-tracked into main 2026-10-11 17:28:34 +00:00
jschoubben 98d494619f mesh-issues: take a delivery's id as mesh-delivery forms it, owner/repo@commit (hq issue 475)
mesh/merge-gate pass: builds mesh-issues → 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
The delivery link's own pattern refused the slash and the @ every delivery id
carries, so no delivery could be linked either. Mirror mesh-delivery's IDOf.
2026-10-11 19:24:23 +02:00
jschoubben 9cfb5f9edb mesh-knowledge: keep when an owed review is due, say it in the history, and tell the operator once after its last try (hq issue 497)
mesh/merge-gate pass: builds mesh-knowledge → 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 superseded: a newer head of the same pull request
A failed review's try count was already kept with the version, but not when it was asked again, so a 10-minute
pause read as a review lost to a restart; and after the last try only the history and the health said so.
2026-10-11 19:24:07 +02:00
jschoubben 7714f164c3 mesh-issues: take every condition key the controller raises, a delivery's with its slash among them (hq issue 475)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds mesh-issues → 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)
The tracker's own pattern refused the slash a delivery's id carries, so no issue
could track a delivery's condition and the operator's alert kept saying no issue
tracks it. Mirror the controller's key rule instead of a pattern of our own; a
router test proves a slashed key is matched and named end to end.
2026-10-11 19:22:18 +02:00
mesh-admin fccad8c0a6 Merge pull request 'mesh-issues: say a delivered fix left in fixing, and flag an abandoned issue (hq issue 483, ADR 0294)' (#212) from fix/483-a-delivered-fix-is-said into main 2026-10-11 16:52:58 +00:00
mesh-admin 58f077e296 Merge pull request 'restic: a backup taken by hand outlives the next thinning (hq issue 453)' (#215) from fix/453-a-hand-backup-is-kept into main 2026-10-11 16:13:45 +00:00
mesh-admin 0a55831992 Merge pull request 'docker: the module's state is in the mesh's directory, never /var/lib/docker (hq issue 452)' (#217) from fix/452-docker-state-is-its-own into main 2026-10-11 16:07:35 +00:00
jschoubben 7e6889a646 docker: keep the module's state in the mesh's directory, never Docker's own
mesh/merge-gate pass: builds docker → 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
#205's "place": "." resolved to /var/lib/docker, where the node-engine places
nothing (issue 339), so ace refused the apply and put the module back. The state
directory now says "place": "mesh", <root>/mesh/docker, and a test holds every
directory and path of the module out of the container runtimes' data (novox/hq
issue 452).
2026-10-11 18:01:24 +02:00
jschoubben 7211227c76 restic: let a restore point taken by hand go only after a night recorded good, say a missing hand= as a file to push, and say what a failed thinning already let go (novox/hq issue 453)
mesh/merge-gate pass: builds restic → 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
2026-10-11 17:57:43 +02:00
mesh-admin b687a98770 Merge pull request 'mesh-knowledge: the review is shown every link kind propose accepts, and a research effort's status apart from its state (hq issue 492)' (#216) from fix/492-the-review-sees-what-propose-accepts into main 2026-10-11 11:12:39 +00:00
jschoubben a6e2881448 mesh-knowledge: the review is shown every link kind propose accepts and a research effort's status apart from its state (hq issue 492)
mesh/merge-gate pass: builds mesh-knowledge → 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
The first live review sent research 046 back for a touches link the rules never named and a status proposed
that is the record's state, not the effort's. The rules now list LinkKinds, the one list propose holds a link
to, each with its meaning.
2026-10-11 13:07:29 +02:00
mesh-admin 73367192c1 Merge pull request 'Nothing reads decision records from hq: records_decisions retired, reserve asks the knowledge base, an empty import refuses (hq issue 488)' (#214) from fix/488-nothing-reads-records-from-hq into main 2026-10-11 10:55:19 +00:00
mesh-admin 28a9821c2e Merge pull request 'mesh-knowledge: the import keeps every field, takes clauses only from the Decision section's own list, and can repair what it held (hq issue 487)' (#213) from fix/487-the-import-keeps-every-field into main 2026-10-11 10:55:15 +00:00
mesh-admin 94bc18bc3f Merge pull request 'mesh-knowledge: the knowledge base reviews a proposal itself, and serves no review verb (hq issue 425)' (#211) from feat/425-the-knowledge-base-reviews-itself into main 2026-10-11 10:55:10 +00:00
jschoubben 1b7f79f761 mesh-issues: a counter not started never starts without the knowledge base's answer, floor or no floor (review of #214)
mesh/merge-gate pass: builds mesh-issues, mesh-knowledge, records → novox; no bus step; every machine composes with the change as it did without (4 of 4 co…
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
2026-10-11 12:44:45 +02:00
jschoubben 156fda4b33 mesh-knowledge: refresh repairs records the knowledge base drafted, keeping their clauses while their source is unchanged; ADR 0114 and 0148 as fixtures (review of #213)
mesh/merge-gate pass: builds mesh-knowledge → 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 covered: a later merge that contains it was delivered: novox/mesh-catalog@1b7f79f76177 (merged as 73367192 into main, walk plan-17917161183…
2026-10-11 12:44:08 +02:00
jschoubben 6fb827605a mesh-knowledge: bound every review failure per record, make one out of tries loud, and let only its author revise a pending insight (review of #211)
mesh/merge-gate pass: builds mesh-knowledge → 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 covered: a later merge that contains it was delivered: novox/mesh-catalog@1b7f79f76177 (merged as 73367192 into main, walk plan-17917161183…
2026-10-11 12:41:24 +02:00
jschoubben 879bc4bca2 restic: keep a backup taken by hand past the next thinning, until a later night and keep-hand-taken-days, so the restore point taken against a risky change is still there when it is needed (novox/hq issue 453)
mesh/merge-gate pass: builds restic → 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 superseded: a newer head of the same pull request
2026-10-11 12:28:17 +02:00
jschoubben d6148a5ba7 mesh-issues: call an issue its worker left abandoned, not a stale claim (hq ADR 0294)
mesh/merge-gate pass: builds mesh-issues → 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
The glossary keeps claim for a module's on a seat.
2026-10-11 12:26:57 +02:00
jschoubben 2369098303 Nothing reads decision records from hq: records_decisions retired, reserve asks the knowledge base, an empty import refuses (hq issue 488)
mesh/merge-gate pass: builds mesh-issues, mesh-knowledge, records → novox; no bus step; every machine composes with the change as it did without (4 of 4 co…
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
Once the records leave hq (issue 426), records_decisions would answer every topic empty, reserve would read 0
and hand out 0001 from a counter not started, and import would say it created nothing as a success. reserve now
starts and raises its counters above mesh-knowledge's knowledge_highest, the floor alone starting one (with a
warning) when it is silent and nothing spent with neither.
2026-10-11 12:25:58 +02:00
jschoubben 3aaccba4e2 mesh-issues: read merged from the pull request, not the state, so a closed one is withdrawn (hq issue 483)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds mesh-issues → 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 says stopped for a pull request closed unmerged too; read as
merged, it was said stuck to the operator for good. Cite ADR 0284 for the
worker rules, which the comments gave as 0283.
2026-10-11 12:24:45 +02:00
jschoubben 601eed06e7 mesh-issues: test that a pull request closed unmerged is withdrawn, not stuck (hq issue 483) 2026-10-11 12:23:42 +02:00
mesh-admin e2d25b2248 Merge pull request 'docker: log rotation is a preference, 100m × 10 by default, judged before daemon.json takes it (hq issue 452)' (#205) from fix/452-log-rotation-is-a-preference into main 2026-10-11 10:21:53 +00:00
mesh-admin 0557be174a Merge pull request 'restic: a failed backup, a failed thinning and a refused retention raise a condition (hq issue 454)' (#210) from fix/454-a-failed-backup-is-raised into main 2026-10-11 10:18:25 +00:00
jschoubben 1cc0ab369f mesh-knowledge: the import keeps every field and takes clauses only from the Decision section's own list, and can repair what it held (hq issue 487)
mesh/merge-gate pass: builds mesh-knowledge → 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 superseded: a newer head of the same pull request
The import at hq f0892019 lost reconstructed, amended and a research effort's touches, held rests-on only as a
citation, took eight records' clauses from a numbered list inside a ### subsection, and left the 11 records with
no Decision section clause-less with nothing queued. Each is held now; refresh repairs a record only the import
ever wrote, and never one changed since.
2026-10-11 12:18:03 +02:00
jschoubben d81e7d063b mesh-issues: say a delivered fix left in fixing, and flag a stale claim (hq issue 483)
mesh/merge-gate pass: builds mesh-issues → 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 superseded: a newer head of the same pull request
The operator found 13 issues in fixing whose fix was delivered hours before:
their pull requests were linked, not in fixed-by, so their delivery was never
measured, and nothing said an issue left there or a worker gone.
2026-10-11 12:16:59 +02:00
jschoubben cbe9fa7a2c restic: raise a night that never happens, and have the mesh write the retention readable (novox/hq issue 454)
mesh/merge-gate pass: builds restic → 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
A hung night, a lock held for hours or a lock that cannot be taken left no
failed night behind, so it stayed healthy. The night's process now counts
how long the machine was up since its newest good night, by the monotonic
clock so a sleeping laptop is not blamed, and past 26 hours it is raised.

The retention file is written 0644 and root's instead of read through
sudo: the account reads it and still cannot change it. An unreadable file
names its cause, and the healthy answer says only what was seen.
2026-10-11 12:14:54 +02:00
jschoubben c4ea58aeb0 mesh-issues: test that a delivered fix and a stale claim are said (hq issue 483)
Each test fails on main: a linked pull request is never measured, nothing
warns of a delivered fix left in fixing, and list has no flag to find either.
2026-10-11 12:14:42 +02:00
jschoubben ebba63d71f mesh-knowledge: review every proposal in a judgement of its own, and serve no review verb (hq issue 425, ADR 0290 as updated)
mesh/merge-gate pass: builds mesh-knowledge → 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 superseded: a newer head of the same pull request
The mesh proves which machine called, not which agent on it (hq issue 365), so a review verb could not tell a
reviewer from the author. Each version proposed is now judged by claude-code.claude_code_judge in a separate
call that sees only the record, the records it links and the rules: approve accepts (or asks the operator for a
needs-operator record), changes keeps it proposed with the findings for its author, and a judgement that cannot
be had or read leaves it proposed, says so, and never counts as approval.
2026-10-11 12:06:18 +02:00
jschoubben d649309895 restic: raise a failed backup, a failed thinning and a refused retention as the module's health (novox/hq issue 454)
mesh/merge-gate pass: builds restic → 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 superseded: a newer head of the same pull request
A failed night was a journal line only, so a typo in a retention setting
let the repository grow until a disk filled. The night and the measuring
now run as processes the node-engine judges: the night by backup_health,
which is unhealthy for a module not backed up, a night that did not run,
a failed thinning or a missing retention file, and waits naming the
setting when one is refused; the measuring by its unit. A file lock
keeps a night and the tool runtime's now and restore apart.

The retention file is root's alone, so the account reads it through sudo:
until now every night would have refused it as unreadable. Every machine
has the file, so a missing one is refused instead of thinned by 14/8/6.
backed-up says the retention in force and the last thinning per module.
2026-10-11 11:56:23 +02:00
mesh-admin 300406e0dd Merge pull request 'mesh-knowledge: run watch as the account the health tool reads as (hq issue 479)' (#209) from fix/479-the-watch-verdict-is-readable into main 2026-10-11 09:55:36 +00:00
mesh-admin 83c64dcd34 Merge pull request 'mesh-delivery: a merge whose walk ended before its word is settled from that walk's record (hq issue 474)' (#208) from fix/474-a-delivery-settles-from-an-ended-walk into main 2026-10-11 09:45:06 +00:00
jschoubben 9d0f93bba8 mesh-delivery: hold a stranded stop for a later merge on its way, and keep it loud (review, hq issue 474)
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 covered after its failure: a later walk that contains it was delivered: plan-1791711936761655786 (novox/mesh-catalog at 83c64dcd into main,…
A stop is final, so a later merge whose queued walk would carry the modules
now holds it off; a stopped one stays in stalled until a later delivered merge
carries them. A walk counts only modules it sent, and an old walk record is read
again rather than taken as building nothing.
2026-10-11 11:38:08 +02:00
mesh-admin 56689847d2 Merge pull request 'mesh-issues: record who acted as the caller the runtime proves (hq issue 365)' (#207) from fix/365-a-tool-call-names-its-caller into main 2026-10-11 09:35:37 +00:00
jschoubben a3f4387e26 Run mesh-knowledge's watch as the account the health tool reads as (hq issue 479)
mesh/merge-gate pass: builds mesh-knowledge → 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
The watch ran as root and wrote its verdict root's and 0600 into the state
directory, which the mesh gives to the account the runtime runs the bundle as;
knowledge_health could not open it, so the module read unhealthy with nothing
wrong. Run watch as that account, as zfs's watch does.
2026-10-11 11:35:02 +02:00
jschoubben 084acc042b mesh-delivery: a merge whose walk ended before its word is settled from that walk's record (hq issue 474)
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 superseded: a newer head of the same pull request
A batch's walk the controller judged to move nothing is done when it is cut,
and a merge it carried stayed published waiting for a start that cannot come.
Its walk's record now settles it: delivered or covered when that walk built its
modules, covered when a later delivered merge carried them (ADR 0270), and
otherwise stopped with why once the cover test says none did.
2026-10-11 11:31:43 +02:00
jschoubben f134ed5910 mesh-issues and mesh-knowledge: pin mesh-sdk at its merge of CallerOf; mesh-knowledge reads the caller from the runtime (hq issue 365)
mesh/merge-gate pass: builds mesh-issues, mesh-knowledge → 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 covered: a later merge that contains it was delivered: novox/mesh-catalog@a3f4387e2630 (merged as 300406e0 into main, walk plan-17917125412…
Both pin mesh-sdk/go at 4cf45b5 (mesh-sdk#20 merged). mesh-knowledge's one
reading of the caller, callerOf, now answers stdio.CallerOf, so a call on the
caller-named subject is recorded as its caller's bus user and any other as
unproven, never from the arguments.
2026-10-11 11:26:45 +02:00
jschoubben 66068febb0 mesh-issues: record who acted as the caller the runtime proves (hq issue 365)
The tool runner now takes a call's caller from the subject it arrived on and
hands it to the module through the SDK (stdio.CallerOf); the tracker records
that bus user on what each verb writes instead of unproven. Never from the
arguments, which still refuse any caller, and unproven where the runtime proves
none. Needs mesh-sdk's go/caller.go (pinned at its branch head until merged).
2026-10-11 11:22:41 +02:00
mesh-admin 9355efa00b Merge pull request 'mesh-knowledge: the knowledge base, first slice (hq issue 425, ADR 0290)' (#206) from feat/425-the-knowledge-base into main 2026-10-11 03:48:50 +00:00
jschoubben ec00e13e22 Hold a decision record to the words rules only from 2026-10-11 (review of #206)
mesh/merge-gate pass: builds new: modules/mesh-knowledge, sent nowhere; 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 delivered
hq's words check never read a decision record, so a correction to any decision
of October 7 to 10 was refused for words it was allowed to write. The links an
accepted record holds are kept by its correction without being judged again.
2026-10-11 05:14:37 +02:00
jschoubben 10ab959d0f Tighten who writes and reviews a record, date the rules, bound history (review of #206)
mesh/merge-gate pass: builds new: modules/mesh-knowledge, sent nowhere; 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 superseded: a newer head of the same pull request
A revision came from anyone and a review was refused only to the first author,
so a co-writer could approve, and a record proposed before callers are proven
would later be reviewable by its own unproven author. Now a revision is taken
only from the proven author, a reserved number only from whoever reserved it,
and a review is refused from any writer of any version or for any record with
an unproven writer. The rules held records from no date, so a correction to an
older record was refused by rules hq never held it to; each dated rule now holds
from hq's own start date, and a new record cannot be dated before its
reservation. history and the question to ask are bounded like the other answers.
2026-10-11 05:10:02 +02:00
mesh-admin 9afc253e85 Merge pull request 'mesh-issues: the tracker hands out decision and research numbers (hq issue 424)' (#204) from fix/424-the-tracker-hands-out-decision-and-research-numbers into main 2026-10-11 02:59:55 +00:00
jschoubben ba7b455719 Add mesh-knowledge, the knowledge base's first slice (hq issue 425, ADR 0290)
mesh/merge-gate pass: builds new: modules/mesh-knowledge, sent nowhere; 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 superseded: a newer head of the same pull request
An agent learns what a decision says only by reading whole records. This module
holds every decision record and research effort whole in a database in the store,
with clauses that carry their own status and source, typed links read from both
ends, and every version. It answers ask, get and holds within a stated bound, and
takes a record through reserve, propose (refused on any of the 29 per-record
rules), review by someone other than the author, and acceptance, on the
operator's proven warrant when the record needs it. The 4 whole-set rules make
the module unhealthy when they fail. The import reads hq at a commit through the
records module, never overwrites, and changes nothing when run again. Prose
decisions get drafted clauses that a second judgement must confirm before they
are served.
2026-10-11 04:58:35 +02:00
jschoubben 9a1a97cc43 docker: make log rotation a preference, 100m x 10 by default, judged by a step before daemon.json takes it (novox/hq issue 452)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/merge-gate pass: builds docker → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1791734868297400826 (novox/mesh-catalog at 0a558319 into main,…
The controller pastes a setting into daemon.json as text and the runtime is its
only reader, so a bad size would surface as a runtime that does not start and a
quote could add any key. A step refuses the value first and gates the module.
The defaults are the one hand-set machine's, so it does not change; nothing
restarts the runtime, which takes the keys at its next start.
2026-10-11 04:43:14 +02:00
jschoubben 9d4d9d5cd2 mesh-issues: say in reserve's answer when hq could not be read, and grant the import its records calls (review of #204)
mesh/merge-gate pass: builds mesh-issues → 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 number handed out unchecked against hq is a failure that must be loud, so
the answer carries a warning, not only the log. The import already calls
records_status and records_read; invokes now names them.
2026-10-11 04:30:28 +02:00
jschoubben 350a597c47 mesh-issues: hand out decision and research numbers, so sessions stop taking them by reading hq (hq issue 424)
mesh/merge-gate pass: builds mesh-issues → 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 superseded: a newer head of the same pull request
A new verb, issue-tracker.reserve {kind, for} -> {kind, number}: one counter
per sequence beside the issue counter, advanced the same way, started above
what hq's record holds, a floor setting for open branches and the
reservations kept, and raised when hq holds a higher number. A reserved
number is spent forever, kept with who, when and for what, and said as the
event reserved (hq ADR 0275 rule 8, ADR 0290 rule 5).
2026-10-11 04:25:48 +02:00
mesh-admin 33ee89794e Merge pull request 'restic: the backup retention is a preference, today's 14/8/6 its default (hq issue 416, first half)' (#200) from fix/416-retention-and-log-rotation-are-preferences into main 2026-10-11 02:24:36 +00:00
mesh-admin 27cb95e98f Merge pull request 'mesh-delivery: a member after a superseded one waits while a later merge can carry it (hq issue 445)' (#203) from fix/445-a-carried-member-lets-the-next-take-its-turn into main 2026-10-11 02:18:42 +00:00
mesh-admin 5eca660fd0 Merge pull request 'mesh-delivery: give an unanswered line's checks as data (issue 443)' (#201) from fix/443-a-stalled-lines-times-are-local into main 2026-10-11 01:52:00 +00:00
jschoubben 15a24fb63e mesh-delivery: share a carrier's walk only when it delivers the whole superseded member
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 turn given on one carrier's waiting walk let the next member go while
another module of the member before it waited on a walk that could still
fail, and the stop then took the shared walk down. The turn is now shared
only when that one walk carries every module not yet carried, each by a
merge the forge said contains the member's; an unknown answer is asked and
waited for (novox/hq issue 445, re-review of #203).
2026-10-11 03:46:32 +02:00
jschoubben f7ba413c10 mesh-delivery: a member on its carrier's walk takes its turn, and a waiting member says for whom
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 superseded: a newer head of the same pull request
Under ADR 0276 the merge that carries a superseded member is often folded
into the next member's own walk, which waits for that member's turn: waiting
there waited for itself until the carrier's bound. The members on that walk
now take their turn, as members sharing one walk do. A member that waits says
which merge it waits for and until when, and `stalled` uses that bound; a
carrier still in a batch is bounded by the window and one walk; a member every
module of which was carried is reached, never stopped with no reason
(novox/hq issue 445, review of #203).
2026-10-11 03:41:52 +02:00
jschoubben 51a85c5998 mesh-delivery: a member after a superseded one waits while a later merge can carry it
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 superseded: a newer head of the same pull request
Superseded is no failure: the later merge that took its walk over may still
deliver its change, and a covered member counts as delivered (ADR 0270). So the
members after it wait while such a merge is open within its own bound, take
their turn once its delivered walk covers the member, and are stopped, saying
why, only when none can carry it (novox/hq issue 445).
2026-10-11 03:34:04 +02:00
mesh-admin 87b0d3a528 Merge pull request 'A pending merge check says where it is: in the build queue since when, then building on which machine (hq issue 446)' (#202) from fix/446-a-pending-check-says-where-it-is into main 2026-10-11 01:27:06 +00:00
mesh-admin a5b4a757ec Merge pull request 'mesh-delivery: say a ready or rejected delivery whose check has not answered on the forge (hq issue 441)' (#199) from fix/441-a-held-delivery-without-its-check-is-said into main 2026-10-11 01:26:37 +00:00
jschoubben 1cf56688c1 Make the gitea module the one writer of mesh/merge-gate, so a verdict always lands last (hq issue 446)
mesh/merge-gate pass: builds gitea → 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 covered after its failure: a later walk that contains it was delivered: plan-1791682027628755615 (novox/mesh-catalog at 87b0d3a5 into main,…
mesh-delivery read the gate as pending and then wrote "building on", and the gitea module could write the verdict in
between: the forge keeps the newest status, so a pass read pending and a failure hid under "building". The gitea
module now hears the build seat's started itself and puts every write of a head's gate on one chain, marking a
verdict the moment it is heard; a start from before the pending words were last set is not the current check's.
2026-10-11 03:23:52 +02:00
jschoubben 4254d17322 Say where a pending merge check is: queued since when, then building on which machine (hq issue 446)
mesh/merge-gate pass: builds gitea, 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 superseded: a newer head of the same pull request
The forge's holder set mesh/merge-gate pending as "the mesh is mapping this head onto its module graph" and nothing
changed it until the verdict, so a ten-minute controller check read as a stalled forge (issue 423). The holder now
says the head waits in the build queue since the local time it was announced; mesh-delivery follows the build
seat's started event and says the machine building it and since when, never over a verdict the forge holds.
2026-10-11 03:14:06 +02:00
jschoubben 30788c46a1 mesh-delivery: give an unanswered line's checks as data, so the controller says their times in the operator's zone (novox/hq issue 443)
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
mesh/delivery-group group fix/443-a-stalled-lines-times-are-local delivered: every member is delivered
2026-10-11 03:10:22 +02:00
jschoubben ecf6d7803f restic: say in each retention setting that lowering it deletes restore points for good, so the operator sees the cost before setting it (novox/hq issue 416)
mesh/merge-gate pass: builds restic → 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
2026-10-11 03:09:46 +02:00
mesh-admin ebd940fcd0 Merge pull request 'mesh-cli: build on the SDK that reads the engine's refusal after a failed write (hq issue 444)' (#198) from fix/444-mesh-cli-hears-the-refusal into main 2026-10-11 01:04:52 +00:00
jschoubben 39d5b25f95 restic: take the retention from the machine's settings, so a disk can keep more or fewer restore points without a new build (novox/hq issue 416)
mesh/merge-gate pass: builds restic → 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 superseded: a newer head of the same pull request
The keep-daily, keep-weekly and keep-monthly preferences default to
today's 14, 8 and 6. The night reads them from a file in its state and
refuses anything but a count from 1 to 9999: it then thins nothing and
says which setting is wrong, never handing a raw value to restic.
2026-10-11 03:04:52 +02:00
jschoubben 9bd8903ed2 mesh-delivery: test that an unanswered line clears when its delivery moves on or its pull request closes (issue 441)
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 covered after its failure: a later walk that contains it was delivered: plan-1791682028101285377 (novox/mesh-catalog at a5b4a757 into main,…
The review asked for the clearing paths to be pinned: a recheck or a merge
clears the line before the forge is read again, and a pull request gone from
the open list is no longer said.
2026-10-11 03:03:43 +02:00
jschoubben 5855c41f9d mesh-delivery: say a ready or rejected delivery whose check has not answered on the forge (issue 441)
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 superseded: a newer head of the same pull request
Ready and rejected have no bound, and the watch for unchecked heads skipped
every head with a delivery, so a lost or lagging status write left a pull
request unable to merge with nothing said. Such a head is now said on issue
438's rule: an unanswered line past the table's bound for a check asked and
not answered.
2026-10-11 03:00:23 +02:00
jschoubben 4870c9171e mesh-cli: build on the SDK that reads the engine's refusal after a failed write, so a caller hears busy, not broken pipe (novox/hq issue 444)
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
2026-10-11 02:56:55 +02:00
mesh-admin c6cf5638dd Merge pull request 'mesh-delivery: a group whose members all ended ends, never delivering (hq issue 442)' (#197) from fix/442-a-group-whose-members-ended-ends into main 2026-10-11 00:55:59 +00:00
mesh-admin bc473a7bc8 Merge pull request 'mesh-delivery: say a head whose merge check started and never answered (issue 438)' (#195) from fix/438-a-check-that-never-answered-is-said into main 2026-10-11 00:47:19 +00:00
jschoubben e3f29e6bb4 mesh-delivery: say where GroupState and groupTurns still differ, keep the stopped line whole
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
Review of #197: a carried superseded member is reached in GroupState but
still stops the members after it in groupTurns (hq issue 445); cite ADR
0239 decision 4, which GroupState extends; shorten the stopped reason so
the member's id survives the forge's 140-character status line. Tests
pin one forge ask per pair and CoveredBy/ContainedIn across a read-back.
2026-10-11 02:45:43 +02:00
jschoubben 036544ee0f mesh-delivery: a closed group whose members all ended ends, never delivering
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 superseded: a newer head of the same pull request
A superseded member counts as reached when a later delivered walk on its
trunk carried every module it moves (ADR 0270's covered test); otherwise
the group reads stopped, as groupTurns already treats it. Five groups
read "delivering, 1 of 2 delivered" for days with nothing in progress
(novox/hq issue 442).
2026-10-11 02:39:45 +02:00
jschoubben 450892498d mesh-delivery: an unanswered line names its checks in a field of their own (issue 438)
mesh/merge-gate pass: builds gitea, 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
mesh/delivery-group group fix/438-a-check-that-never-answered-is-said delivered: every member is delivered
The controller's words for the line (mesh-controller #213) name the checks waited
on; a field spares them reading the sentence.
2026-10-11 02:25:48 +02:00
jschoubben 1f48a29495 mesh-delivery: a check that answers keeps the hour running, and a started head is unanswered (issue 438)
mesh/merge-gate pass: builds gitea, 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 superseded: a newer head of the same pull request
Review of #195: the clock restarted on any change in what a head waits on, so a
check answering pushed the line back by the time it took; it now restarts only on
a newer ask. A head whose check started was announced, so its line no longer says
unannounced; pending times are given in UTC like the module's other lines.
2026-10-11 02:22:16 +02:00
mesh-admin b9de001833 Merge pull request 'mounts: list says an armed automount waits, not mounted (issue 437)' (#196) from fix/437-an-armed-automount-is-not-mounted into main 2026-10-11 00:21:31 +00:00
jschoubben 520763e98d mounts: list says an armed automount waits, not mounted, as the look already judges it (issue 437)
mesh/merge-gate pass: builds mounts → 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 autofs line an armed automount leaves in mountinfo made List say
"mounted" while nothing was mounted. One judgement, Mounted.IsAutomount,
now serves List, the look and the checks before a mount.
2026-10-11 02:17:47 +02:00
jschoubben 9d766b675d mesh-delivery: say a head whose merge check started and never answered (issue 438)
mesh/merge-gate pass: builds gitea, 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 superseded: a newer head of the same pull request
The watch for unchecked heads counted any merge-check status as checked, pending
included, so a head left with a pending mesh/merge-gate, or never given
mesh/repo-check, waited unable to merge with nothing saying so. gitea_open_pulls
now gives each head status with its state and time, and the watch says a head
whose required check stays pending or unset past the delivery table's bound for
a check asked and not answered.
2026-10-11 02:16:34 +02:00
mesh-admin 372b6c2323 Merge pull request 'mounts: list reads the root mount's unit (issue 374)' (#194) from fix/374-mounts-list-reads-the-root-mounts-unit into main 2026-10-11 00:13:58 +00:00
jschoubben 99df6b999b mounts: pass unit names to systemctl show after --, so the root mount's -.mount is not read as an option
mesh/merge-gate pass: builds mounts → 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
systemctl refused the whole call on -.mount, and list answered every mount
without its unit state (novox/hq issue 374). The test fake now refuses a
dash-led argument as systemctl does, so it no longer agrees with the mistake.
2026-10-11 02:02:18 +02:00
mesh-admin d856892ced Merge pull request 'gitea: a merge names the head it was reviewed at (issue 381)' (#193) from fix/381-a-merge-names-the-head-it-was-reviewed-at into main 2026-10-10 23:56:30 +00:00
jschoubben c87a4421b9 gitea: a read-back that fails after the merge says the merge happened (review of issue 381)
mesh/merge-gate pass: builds gitea → 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
The second review found the gap it was asked to look for: everything
after the forge's merge could throw — a forge 5xx, a timeout, a commit
named without parents — and the caller got a bare error that never said
the merge had happened.

From the merge on, a read-back that fails or cannot be done is said
beside merged: true, in read_back; only a proven other head is an error,
and that error says the merge happened. A parent named without its sha
is a failed read-back, never an empty head said to have landed.

A fast-forward's commit is taken as the head only when the forge names
it so. That it does is assumed, not seen on this forge, so another sha
is said in read_back rather than raised as another head having landed.

The method's description lists all five methods and says which two can
be read back. The fast-forward test now gives the merge commit a
different second parent, so reading it as a merge would fail there.
2026-10-11 01:35:43 +02:00
jschoubben 9ef810d9c3 gitea: twelve characters name a head, and what landed is read from the merge commit (review of issue 381)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds gitea → 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)
The review of #193 approved it with five nits, all taken.

Seven characters of a sha can be matched by a commit made to match them
in seconds, so an author who force-pushes could have a review merge a
commit nobody read. A head is now named by at least 12 characters, up to
64 for a sha-256 repository.

What landed was read from the forge's record of the pull request, which
is the very thing a race leaves behind. It is now read from the merge
commit: its second parent for a merge, the commit itself for a
fast-forward. A squash or a rebase makes new commits that name no head,
and the answer says it cannot be read back instead of staying silent.

A merge of another head than the one handed over was reported as a
field beside merged: true, which a caller reading only that would miss.
It is now an error that says the merge happened and what landed.

The refusal no longer asserts that the head moved after the review,
which it cannot know; it says the head is not the one named.
2026-10-11 01:33:17 +02:00
jschoubben c32eb6237d gitea: a merge names the head it was reviewed at (issue 381)
mesh/merge-gate pass: builds gitea → 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 superseded: a newer head of the same pull request
mesh-controller#197 was reviewed at 065cfa0d, force-pushed to 3ced96f
at 12:14 UTC, checked green there at 12:22, and merged at 3ced96f at
12:26. The merge tool kept its promise that only a checked head is
merged, but it took no argument for the head the review approved, so it
merged whatever the head was and a commit nobody reviewed landed.

gitea_merge_pull_request now takes `head`, the commit the caller
reviewed (a sha, or at least its first 7 characters), and refuses with
nothing sent when it is missing, is not a commit, or is not the pull
request's head — naming the head it has now. The forge is handed the
same head as head_commit_id, so a push between the read and the merge
is the forge's to refuse. The answer reads back what was merged and says
so loudly if the forge merged another head anyway.

`head` is required, not optional: an optional guard is the shape that
let this happen. A caller that leaves it out is told what to name.
2026-10-11 01:28:52 +02:00
mesh-admin fa6360c1d3 Merge pull request 'messenger: read a condition's message before it is sent (issue 422)' (#192) from fix/422-a-condition-message-read-before-it-is-sent into main 2026-10-10 22:54:34 +00:00
jschoubben 15546f9d83 messenger: count the store's writes, and name an ask's record when it is the only one held (review of issue 422)
mesh/merge-gate pass: builds messenger → 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
The review's two nits. A write of bytes identical to what is stored was
invisible to a comparison of content, so the test store counts its
writes and the preview's tests read that count: a persist from inside
Preview now fails five of them instead of one. And a router holding an
ask's record and no condition said it held none, rather than saying what
that key is; the branches are the other way round, with a test that
holds nothing else.
2026-10-11 00:49:38 +02:00
jschoubben d00cc8baf4 messenger: the preview is the sending path's own walk (review of issue 422)
mesh/merge-gate pass: builds messenger → 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 superseded: a newer head of the same pull request
The review of #192 found three faults, all verified by running.

The tests did not prove the claim the change leads with: a probe that
sent, queued, recorded and wrote from inside Preview still passed,
because the condition they used was a warning needing nothing, which is
held rather than sent. They now hold an urgent condition, snapshot what
a send would move — the channels, the queue, the history, the refusals,
the stored record and the record held — assert none of it moved, and
tick once more. Every one of those probes now fails the tests.

`cleared` and `silenced` said wording no send would carry: an open
record has no clearing time, so a condition open four hours cleared
"after a moment", and no silence, so a silence read "until Mon 1 Jan
00:17". A clearing is now composed as if it cleared now, which the
answer says; a silence is refused unless the condition is under one.

The preview composed only its own words, while a send reduces a message
whose words the content rule refuses. The escalation walk is now one
function, allowedWords, which `words` calls for a send and the preview
calls alone — so the words read are the words that would leave, and
reading them records no refusal and states nothing on the bus.
withTracking hands back the line it put in, so the preview reports that
line instead of deciding the rule a second time; and a record that is
not a condition, or one carrying an open ask, is refused or says that a
channel's message carries more.
2026-10-11 00:43:40 +02:00
jschoubben b7b34ddad7 messenger: read a condition's message before it is sent (issue 422)
mesh/merge-gate pass: builds messenger → 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 superseded: a newer head of the same pull request
The words a condition reaches the operator with are composed in the
router, and nothing kept them: a send's history row holds a key and an
outcome, never text, and a notification still on the desk is not in the
notifier's history. So a wording could only be proven by waiting for a
send and reading the operator's screen, which is why proving issue 400's
tracking line stalled.

messenger_preview composes what a send would carry for a condition the
router holds open — the same words, the same tracking line, the same
verdict — and says whether the content rule would let them out. It
sends, queues and records nothing.
2026-10-11 00:30:32 +02:00
mesh-admin 7534167fd0 Merge pull request 'mounts: an SMB source's username in its sources entry, its password as its own one-line secret, and a missing one waits (issue 386, ADR 0283)' (#190) from feat/386-a-wait-for-the-operator into main 2026-10-10 20:18:32 +00:00
mesh-admin d91985cc35 Merge pull request 'Say in a condition's message which issue tracks it and where it stands (issue 400)' (#191) from feat/400-alert-names-its-issue into main 2026-10-10 20:09:42 +00:00
jochen 12619ec140 Check that tracking says no event and writes nothing, as ADR 0285 states
mesh/merge-gate pass: builds mesh-issues, messenger → 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
mesh/delivery-group group feat/400-alert-names-its-issue delivered: every member is delivered
2026-10-10 21:51:30 +02:00
jochen a7fc89ef04 Name issues not closed first, and say a quiet worker before a decision, as ADR 0285 says
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds mesh-issues, messenger → 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)
The router's order put a wontfix issue before an open one, and a stale
worker on an issue waiting for a decision said both; review of #191.
2026-10-10 21:50:54 +02:00
jochen 63dba87587 Say in a condition's message which issue tracks it and where that issue stands (issue 400)
mesh/delivery-group group feat/400-alert-names-its-issue checking: 0 of 2 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds mesh-issues, messenger → 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)
The operator could not tell from a notification whether its cause was
known, tracked and being fixed, or whether they had to act. The issue
tracker gains the verb tracking, answering for every condition key an
issue links which issues link it and where each stands; the operator
channel's router asks it outside the work and says one line from it in
every condition's message, per hq ADR 0285.
2026-10-10 21:44:53 +02:00
jochen a12b5908aa mounts: name a malformed entry of sources by its position, never by its name
mesh/merge-gate pass: builds mounts → 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 password holding '=' and a space or comma splits the entry, and the tail
read as name=... was said as "the source <tail>", carrying part of the
secret into the health answer and the operator's conditions (review of
mesh-catalog#190, issue 386).
2026-10-10 21:38:17 +02:00
jochen 58ec352b8c mounts: say nothing an entry of sources holds when it cannot be read
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds mounts → 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)
A password typed into an entry by mistake, in another scheme or split by a
space or a comma, was quoted back in the problem and so reached the status
and the health answer. An entry is now named by its source's name or its
position only; a user with a control character or a second @ is refused
(review of mesh-catalog#190, issue 386).
2026-10-10 21:33:47 +02:00
jochen 6f78899827 mounts: take an SMB source's username from its entry of sources, not a setting of its own
The live module check refused smb-users: a key naming a user is the
operator's value and may have no default, and a setting without one would
refuse to compose mounts on every node until set. The username now goes in
the source's URL (smb://<user>@<host>/<share>), percent-decoded; a
password there is refused unsaid, and a source without a user waits for
sources (hq ADR 0283, issue 386).
2026-10-10 21:30:31 +02:00
jochen c620173966 mounts: read a password only from the directory the configuration names, and say a domain goes in the username
mesh/merge-gate fail: builds mounts → shanks; no bus step; a manifest the change touches fails the module check: modules/mounts/module.json: this manifest …
mesh/repo-check fail: its merge-check.sh failed: files asking for a setting without saying whether it is trusted: 24
mesh/delivery superseded: a newer head of the same pull request
A configuration without the passwords directory would have read a path
relative to the process's working directory; it is now refused as a fault.
Review of mesh-catalog#190 (issue 386, ADR 0283).
2026-10-10 21:14:03 +02:00
mesh-admin 61dcdf03a0 Merge pull request 'mesh-issues: an issue says who works on it, and resolves only from fixing (hq issue 399)' (#189) from feat/399-fixing-status into main 2026-10-10 19:13:32 +00:00
jochen bbb3161ade mounts: take an SMB source's username as a setting and its password as its own secret, and wait for them
mesh/merge-gate fail: builds mounts → shanks; no bus step; a manifest the change touches fails the module check: modules/mounts/module.json: this manifest …
mesh/repo-check fail: its merge-check.sh failed: files asking for a setting without saying whether it is trusted: 24
mesh/delivery superseded: a newer head of the same pull request
The one INI secret smb-credentials could not be given through the desk
prompt, which takes one hidden line, and a missing login was said as a
fault: the first-node gate put the build back and the operator was told
nothing was theirs to do. The username is now the setting smb-users and
each password the one-line secret smb-password-<source>; a source missing
either answers waits through mesh-sdk's health answer (hq ADR 0283,
issue 386, issue 379).
2026-10-10 21:11:16 +02:00
jochen a73b04843d mesh-issues: a take-over note records who acted, not the worker named; tests for the remaining rules (hq issue 399)
mesh/merge-gate pass: builds mesh-issues → 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
mesh/delivery-group group feat/399-fixing-status delivered: every member is delivered
The note's by is who acted as the runtime proves it (ADR 0275 §5); the
note's text names both workers. Tests for a resolved issue corrected, link
and set keeping the worker current, and a take-over reason with a secret's
shape. The ready-to-verify note says an issue resolves from fixing.
2026-10-10 21:10:36 +02:00
mesh-admin 6f2c08c66e Merge pull request 'mesh-delivery: delivery time per delivery and the times verb (hq ADR 0282 slice 1, issue 382)' (#188) from feat/382-delivery-times into main 2026-10-10 19:07:24 +00:00
jochen 11afde55d6 mesh-issues: an issue says who works on it, and resolves only from fixing (hq issue 399)
mesh/delivery-group group feat/399-fixing-status checking: 0 of 2 member(s) ready
mesh/merge-gate pass: builds mesh-issues → 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 superseded: a newer head of the same pull request
Two agents could start on one issue without seeing each other, and an issue
could be resolved without anyone having said they started its fix. A move to
diagnosing or fixing now names by, the worker as who@node; get and list show
the worker with since and stale; another worker is refused while it is
current unless take-over says why (noted); a worker untouched for
worker-stale-after (24h) may be taken over from without a reason. Resolved is
reached only from fixing. The moved event carries by, since and caller.
Fixing counts as not closed for the triage and the duplicate check.
hq ADR 0283.
2026-10-10 21:04:32 +02:00
jochen 08dab94bb0 mesh-delivery: an unreliable time names its phases with no number; tests that it is never counted and that phases short of the walk's end are unreliable
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
2026-10-10 20:56:50 +02:00
jochen 662c4105d3 mesh-delivery: cut a walk's phases at the delivery's own merge, never a negative time; moments out of order are unreliable, not a number (hq issue 382)
mesh/delivery superseded: a newer head of the same pull request
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)
2026-10-10 20:55:21 +02:00
jochen 383e698254 mesh-delivery: rebase only a first phase that starts at the walk's merge; a class without a budget is said unknown
mesh/merge-gate fail: builds mesh-delivery → novox; no bus step; a manifest the change touches fails the module check: mesh-delivery claims mesh-delivery a…
mesh/repo-check fail: its merge-check.sh failed: files asking for a setting without saying whether it is trusted: 25
mesh/delivery superseded: a newer head of the same pull request
2026-10-10 20:16:46 +02:00
jochen 404efb764b mesh-delivery: keep each delivery's time from its merge and answer times per class (hq ADR 0282, issue 382)
mesh/merge-gate fail: builds mesh-delivery → novox; no bus step; a manifest the change touches fails the module check: mesh-delivery claims mesh-delivery a…
mesh/repo-check fail: its merge-check.sh failed: files asking for a setting without saying whether it is trusted: 25
mesh/delivery superseded: a newer head of the same pull request
The operator's budget (a leaf module running everywhere within five minutes
of its merge, a core module within ten) cannot be held to without measuring
it. Each delivery now keeps its delivery time, class and phases from the
walk the controller says; 'times' answers the week's median and worst per
class, the deliveries over budget and their longest phase, and one
delivery's breakdown; 'deliveries' is headed by the same line. A phase the
walk could not measure stays unknown, never a zero. Measurement only.
2026-10-10 20:14:19 +02:00
mesh-admin a14fa306f2 Merge pull request 'mesh-delivery: write a delivery only when it changed (hq issue 326)' (#187) from fix/326-write-a-delivery-only-when-it-changed into main 2026-10-10 17:50:53 +00:00
jochen 7eb96f2a1d mesh-delivery: write a delivery only when it changed (hq issue 326)
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
keep wrote the whole delivery on every call, and Reconcile passes every walk
the controller keeps through settleAndKeep every 30 seconds, so the
deliveries bucket was rewritten about once per walk per pass with nothing
changed: a key's revision no longer meant the delivery moved, and a watcher
of the bucket would receive a stream of non-changes.

keep now remembers each delivery as it last wrote it, encoded, and skips the
write when the delivery encodes exactly the same and is not waiting to be
written again. Anything a delivery holds that changes - its state, steps,
walk record, owed effects and their tries - changes its encoding, so every
change is still written; a failed write stays dirty and Flush writes it
again. Events, notes, views and forge statuses are owed and done as before.
A retry of an owed effect still writes its new try count: it is a change.

The test fails before (five passes over unmoved walks wrote five times) and
passes after; a walk that moves is still written and said.
2026-10-10 19:46:25 +02:00
mesh-admin 63412ec36a Merge pull request 'mesh-issues: a verdict naming issue 0 is none, and the tracker's own module is a registered owner (issue 396)' (#186) from fix/triage-issue-zero-is-none into main 2026-10-10 17:22:25 +00:00
jochen 383194488b mesh-issues: only a duplicate or related verdict naming 0 is none (review, issue 396)
mesh/merge-gate pass: builds mesh-issues → 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
2026-10-10 18:28:45 +02:00
jochen 6bd69ebadc mesh-issues: a verdict naming issue 0 is none, and the tracker's own module is a registered owner (issue 396)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds mesh-issues → 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)
The model says 'no issue' as related to 0; the triage took that as an issue not
offered and failed the issue instead of triaging it.
2026-10-10 18:27:53 +02:00
mesh-admin 7e7dfbfb1a Merge pull request 'mesh-issues: an issue knows whether its fix is delivered, says it ready to verify, and a fix stuck after its merge is said (hq ADR 0281)' (#185) from feat/mesh-issues-delivery into main 2026-10-10 16:19:55 +00:00
jochen 01bde68f96 mesh-issues: an issue knows whether its fix is delivered, says it ready to verify, and a fix stuck after its merge is said (hq ADR 0281)
mesh/merge-gate pass: builds mesh-issues → 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
The operator approved measuring from the delivery's own events whether each fix an
issue names reached the machines; delivered is not resolved, and a merged fix that
does not arrive is said on the issue and the operator's channel instead of found.
2026-10-10 18:12:52 +02:00
mesh-admin 0b4ae8097f Merge pull request 'mesh-issues: triage every new issue — severity, needs, suggested owner, dependencies — and ready when what it depends on is delivered (hq ADR 0279)' (#184) from feat/mesh-issues-triage into main 2026-10-10 16:11:57 +00:00
jochen 9bb8253609 mesh-issues: triage every new issue — severity, needs, suggested owner, dependencies — and say an issue ready when what it depends on is delivered (hq ADR 0279)
mesh/merge-gate pass: builds mesh-issues → 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
The operator asked for the judgement that already reads the backlog to fill what an
issuer cannot know, and for dependencies between issues, an issue being workable
only once what it waits for is on the machines. The triage fills only empty values,
adds links and removes none; readiness follows mesh-delivery's own events, said once
per spell, through a duplicate to its original, and loud when a delivery cannot be asked.
2026-10-10 17:56:08 +02:00
mesh-admin 105cc7c0de Merge pull request 'dnsmasq answers or is restarted, binds once; tools for DNS, sockets and the runtime's memory (hq issue 348)' (#161) from fix/the-resolver-answers-or-is-restarted into main 2026-10-10 15:48:12 +00:00
jochen a10f1844f2 Merge branch 'main' into fix/the-resolver-answers-or-is-restarted
mesh/merge-gate pass: builds dnsmasq, docker, netcheck → ace, g14, novox, shanks; no bus step; 2 wait(s) for a person; every machine composes with the chan…
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
2026-10-10 17:45:33 +02:00
mesh-admin 25c7bee3b8 Merge pull request 'mesh-issues: the duplicate check on every new issue, and groom for the backlog; claude-code: claude_code_judge (hq ADR 0278)' (#183) from feat/mesh-issues-groom into main 2026-10-10 15:34:47 +00:00
jochen a5c38892a7 mesh-issues: review — one judgement at a time with new issues first, one verdict applied at a time, a resolved issue left alone on redelivery, the pass's answer a copy (hq ADR 0278)
mesh/merge-gate pass: builds claude-code, mesh-issues → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 o…
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
An independent review found a pass could starve new issues' checks, two writers
could apply one verdict twice, a judged issue resolved by a person could still be
linked, and the dry run's answer raced its pass.
2026-10-10 17:20:07 +02:00
jochen f58840b8a6 mesh-issues: check every new issue for a duplicate; claude-code: one judgement with no tools (hq ADR 0278)
mesh/merge-gate pass: builds claude-code, mesh-issues → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 o…
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
The operator asked for duplicate issues to be consolidated as they are opened.
The tracker consumes its own opened event and asks the agent on its node, through
claude-code's new claude_code_judge, whether an older issue not resolved asks the
same question; it closes only the newer issue of a certain duplicate, keeping its
report on the older one, and fails loudly when it cannot judge. groom runs the
same judgement over the backlog as a dry run, applied on confirm.
2026-10-10 17:11:54 +02:00
mesh-admin fb77be42ca Merge pull request 'A channel shows nothing from its memory after a restart; the router says again what is still open (issue 384)' (#182) from fix/issue-384-restart-replays-nothing into main 2026-10-10 14:17:04 +00:00
jochen 45361b013a Review: keep the desk's notification ids so lost answers are said again in place (issue 384)
mesh/merge-gate pass: builds desk-channel, messenger → 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
dunst.started says the notifier's bundle started again: the notifications stay on screen and only the
answers they waited for are lost. Forgetting their ids made the router's message a second popup beside a
dead one. The desk now keeps the ids and says answers-lost-at; the router's message replaces each
notification in place.

Whether a message is said again is now its own (last said before the answers were lost), so a router
that started after the loss still says it and a repeated standing says nothing. It is sent to the desk
directly, not through the outbox, whose failure path moves a warning to Telegram; a failed send is tried
again at the next standing.
2026-10-10 16:13:57 +02:00
mesh-admin aabb83d6c9 Merge pull request 'mesh-issues: import hq's issue folders at their own numbers (hq ADR 0275, to-be 53 slice 3)' (#180) from feat/mesh-issues-import into main 2026-10-10 14:04:23 +00:00
jochen cd85104988 A channel shows nothing from its memory after a restart; the router says again what is still open (issue 384)
mesh/merge-gate pass: builds desk-channel, messenger → 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 superseded: a newer head of the same pull request
On 2026-10-10 the desk's notifier started again on two machines and the desk showed, from its own
memory, nine old popups: resolved, silenced and cancelled ones among them. Its memory held every handle
ever shown with buttons, and the router's edits (silenced, resolved) had reached only the newest handle
of each problem, so the rest stood as if open.

A channel's memory serves only to replace or edit a message in place. A notifier that started again lost
its screen: the desk forgets that machine's notification ids and says in its standing since when the
screen is new (shows-since). The router, which alone knows what is still open and unanswered, says that
again on that kind: one message per problem under its handle with the words and answers of now, a
silenced one edited to say so with no answers and no urgency, nothing that ended. Each move of
shows-since is acted on once, and waits while the desk cannot send rather than going to Telegram.
Telegram re-shows nothing on a restart (its messages stay in the chat) and says no shows-since.

An edit that offers nothing and reaches no screen its message is on is taken with nothing to change;
one that offers answers is said as failed, so the router says it anew.
2026-10-10 16:02:37 +02:00
jochen d05308a419 mesh-issues: import hq's issue folders at their own numbers, and an issue with a folder is whole in the tracker (hq ADR 0275, to-be 53 slice 3)
mesh/merge-gate pass: builds mesh-issues → 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
2026-10-10 16:00:19 +02:00
mesh-admin 3b497bd71f Merge pull request 'mounts: the SMB login is typed as one line at the desk's prompt (issue 379)' (#179) from fix/mounts-one-line-smb-login into main 2026-10-10 13:55:03 +00:00
jochen a058cad735 mounts: the SMB login is typed as one line at the desk's prompt (issue 379)
mesh/merge-gate pass: builds mounts → 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 covered after its failure: a later walk that contains it was delivered: plan-1791663540935383995 (novox/mesh-catalog at 7534167f into main,…
The secret smb-credentials was an INI file alone, which the desk's one-line
prompt (secret ask, hq ADR 0277) cannot type: on the desktop the Pi's source
said "line 1 is not key=value inside a [section]". A login is now also one line,
`[recalbox] username=root password=…`, password last and to the end of the
line so it may hold spaces and `=`; the file form still reads, and a file may
mix both. A line the module cannot read is refused with the form it expected,
never with what was typed, and a login without a username is not answered. The
source's "login is not given" says the desk verb and the line to type.
2026-10-10 15:50:56 +02:00
mesh-admin 1fcf46fbfc Merge pull request 'rofi: the desk's secret prompt names who asked (hq ADR 0277)' (#177) from feat/0277-the-desk-prompt-names-who-asked into main 2026-10-10 13:34:21 +00:00
jochen 4b9558f195 rofi: the desk's secret prompt names who asked (hq ADR 0277)
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 controller's secret-ask verb opens the launcher's hidden prompt from anywhere on the mesh, so the
operator must know whose ask they are typing into. The prompt takes an optional asked_by, the
controller's word on the bus's caller held to a name's characters and spaces, and says it; a sentence,
markup or a line break is refused, so the prompt's words stay the launcher's.
2026-10-10 15:17:57 +02:00
mesh-admin 64feabc6c3 Merge pull request 'One message per problem: a reminder replaces, a blip says nothing (issues 373 and 369)' (#176) from fix/373-one-message-per-condition into main 2026-10-10 12:39:06 +00:00
jochen 8dfc19c9f7 A silence is said on the shown message, which then times out (issue 373)
mesh/merge-gate pass: builds desk-channel, messenger, telegram → novox; 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 superseded: its walk was superseded and names no walk that took it over: a walk that took it over answers its merge, and it follows that wa…
mesh/delivery-group group fix/373-one-message-per-condition delivered: every member is delivered
Seen at 14:40: machine.shanks.units was silenced for 7 days with a reason,
and its critical popup stood on the desk as "Still open" while the
router logged nothing. A silence now edits the message where it is shown
to "Silenced until <day>: <headline>" with the reason, no answers and no
urgency, so the desk's popup times out; nothing more is said while it
lasts. When the silence ends and the condition is still open, the message
is edited back to its open words. Resolved while silenced, it is said only
where an edit notifies nobody. The silence is taken from the controller's
event and from the 10-min reading of the open set alike.
2026-10-10 14:32:35 +02:00
jochen f8515219b1 Review: a blip swallows no escalation or waiting reminder; replace safely (issue 373)
mesh/delivery-group group fix/373-one-message-per-condition ready: every member ready, and composed together they pass
mesh/merge-gate pass: builds desk-channel, messenger, telegram → novox; 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 superseded: a newer head of the same pull request
- A comeback before the clearing was said rings when it is urgent now, and
  says the reminder or escalation that was waiting when it cleared.
- Comebacks never said are counted apart, so "again, Nth time" counts only
  what the operator was told; an urgent comeback after a said clearing rings
  again in place of its message.
- telegram sends the new message before deleting the earlier one, so a
  failed send leaves the operator a message.
- desk-channel keeps a machine's notification on record when its replace
  fails, and says a message shown nowhere as failed.
- A store that cannot be read sends a new message instead of holding it.
- Tests for each, and for a reminder in a burst and a clearing the channel
  could not take.
2026-10-10 14:22:53 +02:00
jochen 3345ff9317 One message per problem: a reminder replaces, a blip says nothing (issues 373 and 369)
The operator stopped believing the mesh's notifications: one problem stood
twice on the desk (a reminder beside the message it followed, an escalation
then a reminder 31 s later), a unit failing again at each apply was said
again 22 times, and a question's buttons outlived it on a reminder the
ask's end never reached.

- messenger: a reminder or escalation is said in place of the problem's
  message (Replace under its handle), alone, never as a digest line; a
  problem first said in a digest gets its own message next time. A
  reminder counts from when it was last said. A clearing is said once it
  stays cleared 2 min (10 min once it came back); back before that, the
  message continues without a word. Cleared before ever said and back:
  said. An ask's expiry is said with its day.
- desk-channel: a send or show under a handle already shown replaces its
  notification by the notifier's replace id.
- telegram: the same deletes the earlier message and sends the new one,
  ringing as its kind rings; one it cannot delete loses its buttons.
2026-10-10 14:22:42 +02:00
mesh-admin 6ec02b6eb1 Merge pull request 'messenger, telegram: a tap shows at once what it did, and the question changes to its outcome (issue 372)' (#175) from fix/372-a-tap-shows-its-outcome into main 2026-10-10 12:15:14 +00:00
jochen 6b4cfc3708 telegram: a refusal of a tap on changed words stays on the phone alone (issue 372 review)
mesh/merge-gate pass: builds messenger, telegram → 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
The lab's proof showed it then reached a stranger's chat: whoever the buttons
reached is told nothing more than the tap's own acknowledgement.
2026-10-10 14:09:16 +02:00
jochen 35c4de7d08 messenger, telegram: a second tap on the recorded answer is said as recorded, and the fallback claims nothing (issue 372 review)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds messenger, telegram → 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)
The review found that a double tap while the phone's spinner turned would be
answered 'That answer was not taken', reading as if the approval failed; it is
now 'You already chose Approve at 13:30: recorded.' and does nothing more. The
fallback toast no longer promises a change that may not come (a stranger's
tap), the wait outlasts the router's 10 s wait for the controller's root word,
the channel's own refusals stay in the chat too, and the store's error words
stay in the journal.
2026-10-10 14:05:41 +02:00
jochen 020c05fe65 messenger, telegram: show the operator at once what a tap did, and change the question to its outcome (hq issue 372)
mesh/merge-gate pass: builds messenger, telegram → 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 superseded: a newer head of the same pull request
At 13:30 on 2026-10-10 the operator approved two questions on Telegram and saw
nothing happen: the channel acknowledged each tap with no words, before the
router had judged it, and the edit that removed the buttons kept the question's
title, adding only a line under it that rings nothing.

The router now answers a chosen option in reply to the tap, once the ask's end
is recorded and before the question is edited: "You chose Approve: recorded,
and claude-code was told." (or, when the bus did not take the warrant, that it
is told when it does). A refusal already replied with its reason. Telegram
holds a tap's acknowledgement for the router's word and shows it as the tap's
own toast; a word with a body (a refusal's reason, Details) also stays in the
chat. Without a word in 8 seconds the toast claims nothing, and the late word
goes to the chat. A secret is never a toast.

An ask's own message, once answered, carries its outcome as its title:
"You chose Approve at 13:30 on Telegram · <headline>", then whether the asker
was told. The asks protocol carries no result back from the asker, so nothing
more is claimed of what it did.
2026-10-10 13:59:54 +02:00
mesh-admin f6c9e06cdf Merge pull request 'messenger: every message still shown keeps its own Details (issue 371)' (#174) from fix/messenger-details-per-message into main 2026-10-10 11:48:50 +00:00
jochen 6a8c718f44 messenger: ask whether a channel is ready before composing its messages, so failed tries make no Details tickets (issue 371)
mesh/merge-gate pass: builds messenger → 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
The review found that each try at an unready channel composed its held messages and so made a Details
ticket for each, and enough tries pushed out the tickets of messages already shown.
2026-10-10 13:46:34 +02:00
jochen bb96f4d163 messenger: every message still shown keeps its own Details, bounded in number and age (issue 371)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds messenger → 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)
A newer message about the same problem (an escalation, a reminder, a re-send) replaced the Details
ticket of the one before it on that channel kind, so tapping Details on any but the newest was refused
"it belongs to no open message". Each message now keeps a ticket of its own, used once, at most
MostDetails per record and kind and none older than TokenFor; one pushed out is refused in words that
send the operator to the newest message. A change of the condition's actions no longer drops Details,
which names no action.
2026-10-10 13:44:43 +02:00
mesh-admin 7fd1ceb310 Merge pull request 'claude-code: leave the judging of a proposal's channel to the router, and hear its refusal (issue 370, hq ADR 0274)' (#173) from fix/claude-code-asks-without-the-channel-kinds into main 2026-10-10 11:18:27 +00:00
mesh-admin d2af46b715 Merge pull request 'mesh-delivery: a walk answers every merge it contains, never an ended delivery (hq issue 362, ADR 0276)' (#171) from fix/362-a-walk-answers-every-merge-it-contains into main 2026-10-10 11:14:07 +00:00
jochen 774f18afcf claude-code: an ask taken after its refusal is still answered; the wait is bounded (issue 370 review)
mesh/merge-gate pass: builds claude-code → 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
A refusal heard on the decided subject is kept as heard, and a record of a person's choice found later is
still taken, so an ask the router's queue offered again is not lost in silence. A heard end must match the
proposal's ask; the wait stops reading at its deadline; the heard ends are forgotten when Propose returns.
2026-10-10 13:08:09 +02:00
jochen 13ffae7706 mesh-delivery: a walk answers every merge it carries, never a delivery that ended (hq issue 362, ADR 0276)
mesh/delivery delivered
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)
Two catalogue merges 18 s apart were walked by one walk at the later commit,
which superseded the first; this owner tied that walk to the dunst delivery
the supersession had ended and to no other, so nothing gave it its word and
claude-code was held for a person with a reason naming neither cause.

A delivery now follows the newest walk that answers its merge: one naming it
(delivery.merges, the controller's batches), its own commit, the walk that
took its own over (taken_over_by, or the superseded note), or, from a
controller that names no merges, an open walk of its branch made for a later
merge, moving all it moves, whose commit the forge says contains it. Only its
own walk's record moves an ended delivery. The word is asked once per walk,
when every delivery it carries is at its turn, a group's members on one walk
together. An earlier merge carried by a later commit ends covered with the
walk, and waits for a walk of its own when it fails. A merge no walk answers
is held with the true reason and listed at once; a waiting walk no live
delivery holds is listed as unheld; a merge in an assembling or queued batch
is shown, not held. Replay R362 fails before.
2026-10-10 13:05:50 +02:00
jochen aafda6e870 claude-code: leave the judging of a proposal's channel to the router, and hear its refusal (hq ADR 0274)
mesh/merge-gate pass: builds claude-code → 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 superseded: a newer head of the same pull request
A module that only asks is issued no channel kinds, so the empty list was read as 'no channel proves its
sender' and every proposal was refused on the live mesh with Telegram linked. The router judges; Propose
waits for its word, and a refusal heard on the decided subject discards the proposal on every node.
2026-10-10 13:04:17 +02:00
mesh-admin ac41b6d052 Merge pull request 'mesh-issues: the issue tracker, so an issue's number comes from one counter no two openers share (hq ADR 0275, to-be 53 slice 1)' (#172) from feat/the-issue-tracker into main 2026-10-10 11:00:55 +00:00
mesh-admin c41c32cb71 Merge pull request 'claude-code: an agent proposes a shared instruction section, and every node takes it only on the operator's warrant (hq ADR 0274)' (#170) from feat/claude-code-instructions-proposed into main 2026-10-10 10:56:37 +00:00
jochen 6f2c76ee71 mesh-issues: an open whose write may have landed never says nothing was opened, and a module must itself be registered
mesh/merge-gate pass: builds new: modules/mesh-issues, 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
From the review: a taken key that cannot be read back, or a write never
confirmed, now tells the caller to get the number before opening again; an
owner entry naming a module is owned only by that module registered; link
targets get the secret guard; line separators are refused in one-line fields;
the acceptance test now proves the counter's own updates were contended.
2026-10-10 12:56:16 +02:00
jochen 2493e3e048 Add mesh-issues, the issue tracker, so an issue's number comes from one counter no two openers share (hq ADR 0275, to-be 53 slice 1)
mesh/merge-gate pass: builds new: modules/mesh-issues, 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
The seat issue-tracker, held once for the mesh: open, get, list, move, note
and link. The counter is advanced by the SDK's compare-and-set Increment
(go/v0.1.12) and started once at the setting first-number (365, after hq's
highest), never below an issue kept. The cycle's issue rules are refused at
the write; every change is an event. The runtime does not yet say who called,
so every change is recorded as by "unproven" and no argument can name a caller.
2026-10-10 12:51:15 +02:00
mesh-admin 52f34d362a Merge pull request 'route-proxy and build-agent name the package they compile; a rename is said under both names (hq ADR 0267, issue 363)' (#169) from fix/0267-the-images-name-the-package-they-compile into main 2026-10-10 10:25:29 +00:00
jochen e4edab3fd0 claude-code: refuse variation selectors, blank letters and underlined or HTML headings; re-check every field at render; forget refusals the router no longer keeps (hq ADR 0274 review)
mesh/delivery delivered
mesh/merge-gate pass: builds claude-code → 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)
2026-10-10 03:55:45 +02:00
jochen 490f76e234 claude-code: hold the review's findings on proposed instructions (hq ADR 0274)
mesh/merge-gate fail: builds claude-code → ace, g14, novox, shanks; no bus step; a manifest the change touches fails the module check: claude-code uses the…
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
An @ after any whitespace or punctuation, invisible and control characters, and top headings are refused;
every node judges a proposal record as the module would have made it before applying an approval, never
applies one older than its section's last change, never takes an unreadable record for an empty one, and
keys a node section by its nodes. The controller records the warrant from the router's word alone.
2026-10-10 03:52:03 +02:00
jochen 74e1be97ab claude-code: an agent proposes a shared instruction section, and every node takes it only on the operator's warrant (hq ADR 0274)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate fail: builds claude-code → ace, g14, novox, shanks; no bus step; a manifest the change touches fails the module check: claude-code uses the…
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
Shared instructions were the terminal's alone since hq issue 340, because the tool runner cannot tell an
agent from the operator. The operator channel now proves who answered, so an agent may propose a section;
each node proves the router's warrant against the exact text itself and keeps what it approved in its own
file, so nothing written to the bus becomes active.
2026-10-10 03:40:48 +02:00
jochen f4fb0a7461 Name the package each controller-repository image compiles, and say a renamed file under both names (hq ADR 0267, issue 363)
mesh/merge-gate pass: builds build-agent, gitea, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did witho…
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
Every merge to the controller's repository rebuilt the route proxy and the
build seat's holder, because their builds read that repository whole. Named,
the build seat hands each only its program's build source and the planner
moves each only when that changes. A rename was said under its new name
alone, so a file moved out of a build source changed nothing the planner
could see.
2026-10-10 02:58:31 +02:00
mesh-admin 48bda475e5 Merge pull request 'dunst: a tap chooses a notification's action (hq issue 360)' (#167) from fix/dunst-tap-chooses-action into main 2026-10-09 22:21:09 +00:00
mesh-admin 553b71916e Merge pull request 'claude-code: claude in the operator's zsh starts the agent's session where the machine names an agent account (hq ADR 0266)' (#168) from feat/claude-is-the-agents-session into main 2026-10-09 22:20:51 +00:00
jochen 378c2b2ce8 claude-code: claude in the operator's zsh starts the agent's session where the machine names an agent account (hq ADR 0266)
mesh/delivery covered: a later merge that contains it was delivered: novox/mesh-catalog@e4edab3fd0eb (merged as c41c32cb into main, walk plan-17916298010…
mesh/merge-gate pass: builds claude-code → 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)
Typing claude on the control node should start the session as the agent's own account, not as the
operator. The controller fills no machine fact into shell code, so the account agents run as is
written to /etc/claude-code/agent-account and the zsh code adds alias claude=claude-agent only when
that file names an account other than the shell's own. Where agents run as the operator's account,
nothing is added. claude-agent keeps running the real claude: exec sees no alias.
2026-10-10 00:16:53 +02:00
jochen f055898089 dunst: the menu takes no typed text, so a typed line never becomes a choice
mesh/merge-gate pass: builds dunst → 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
Review of hq issue 360: rofi without -no-custom hands typed text to dunst as if chosen.
2026-10-10 00:11:45 +02:00
jochen f76396de6f dunst: leave the rofi package to the rofi module, which the merge gate requires
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds dunst → 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)
Two modules on one machine may not declare the same package; the rofi module is assigned wherever
dunst is.
2026-10-10 00:11:00 +02:00
jochen 2a16d39a3d dunst: a tap chooses a notification's action, so a touchpad can answer the desk (hq issue 360)
mesh/merge-gate fail: builds dunst → g14, shanks; no bus step; 2 problem(s) the change brings; the first: a machine (f35): nothing could be sent to it with…
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
The actions sat on the middle click, which the operator's touchpads cannot give, and the context
menu named dmenu, which dunst's PATH does not hold. The left click is now do_action, close_current
(one action runs, several open the menu, none closes), the menu is rofi, declared as a package,
Super+N opens it from the keyboard through i3's contribution, and send refuses the token default,
which dunst would run at a tap without the menu.
2026-10-10 00:07:07 +02:00
mesh-admin db84ebf48e Merge pull request 'claude-code: declare the account agents run as and claude-agent (hq ADR 0266)' (#141) from feat/the-agent-account-is-declared into main 2026-10-09 21:59:39 +00:00
jochen b240085e0a Close the review's gaps round the agent account (hq ADR 0266)
mesh/merge-gate pass: builds claude-code, claude-licence-manager, restic, sudo → ace, g14, novox, shanks; no bus step; every machine composes with the chan…
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered
The managed settings apply to every account's claude, so a key-helper script under the
agent's home would run as the operator, and through sudo as root: the command is now
carried whole in the root-owned managed settings and reads the key only as the agent.
The helper reads only a regular file, bounded, without blocking, and a request it does
not answer fails in time instead of holding every later one; a directory entry it
answers that is not a name is refused. The guard's record is matched on its binary in
the query, a half-named agent account is not taken as the operator's, the kept API key
stays until the agent's home holds one, restic refuses to restore beside a directory
others may write, and the agent-home check cleans a path before judging it.
2026-10-09 23:48:15 +02:00
jochen b382045721 Merge remote-tracking branch 'origin/main' into feat/the-agent-account-is-declared
mesh/merge-gate pass: builds claude-code, claude-licence-manager, restic, sudo → ace, g14, novox, shanks; no bus step; every machine composes with the chan…
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery superseded: a newer head of the same pull request
2026-10-09 23:37:04 +02:00
mesh-admin ac7400d20c Merge pull request 'Refuse a daemon's line in what a module shows: the operator types nox (hq ADR 0272, issue 356)' (#165) from fix/356-the-operator-types-nox-lines-checked into main 2026-10-09 17:34:09 +00:00
jochen dd14526870 Refuse a daemon's line in what a module shows: the operator types nox (hq ADR 0272, issue 356)
mesh/merge-gate pass: builds mesh-vault → 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 covered after its failure: a later walk that contains it was delivered: plan-1792148283013560219 (novox/mesh-catalog at 3db85a0d into main,…
A condition told the operator to type the node-engine's binary at the machine, and nothing held the catalogue's texts to ADR 0272. The words check gains retired-lines — the controller's and the node-engine's commands, copied from hq's words.py --list lines, which compares it — and refuses one in any string a module's tools can show or any manifest description. The vault's and the route adapter's texts now say the nox line.
2026-10-09 18:33:49 +02:00
mesh-admin c07d6890f4 Merge pull request 'mesh-cli: carry what a pipe or a file gives standard input, for the controller's terminal (hq ADR 0259 §10, ADR 0272)' (#164) from feat/mesh-cli-carries-standard-input into main 2026-10-09 15:58:49 +00:00
mesh-admin c7d2eefb50 Merge pull request 'telegram and desk-channel: channels of their own (hq ADR 0259)' (#134) from feat/asks-answered-on-any-channel-channels into main 2026-10-09 15:48:33 +00:00
mesh-admin 5542766ee4 Merge pull request 'rofi: node-launcher's hidden prompt, answered sealed to the asker (hq ADR 0259 §10)' (#135) from feat/a-secret-given-at-the-desk into main 2026-10-09 15:27:11 +00:00
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
jochen ad5a85398e Serve node-launcher's hidden prompt, answering sealed to the asker, so a secret typed at the desk is never in the clear (hq ADR 0259 §10) 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
jochen 0d9ec84a9c Run telegram as its own account in a runtime of its own, keep its offset per update, and keep no copy of a secret on the desk (hq ADR 0259 §8) 2026-10-09 17:08:17 +02:00
jochen a412389725 Add telegram and the desk as channels of their own, each judging nothing and holding only its own secret (hq ADR 0259) 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
jochen a6965448f0 Show a link's code as a message no channel keeps 2026-10-09 17:08:17 +02:00
jochen debb897c21 Run the router as its own account, ask unlinking of the linked account, and take no approval where an agent can become root (hq ADR 0259 §7, §8) 2026-10-09 17:08:17 +02:00
jochen c2c75ceb3b Make the messenger the router: it holds every ask, judges each answer and issues the warrant its asker acts on, and keeps no channel's client (hq ADR 0259) 2026-10-09 17:08:17 +02:00
mesh-admin d902e1550e Merge pull request 'mesh-delivery: cover a held merge whose module a backlog walk carried to every machine (hq issue 351)' (#163) from fix/a-release-carried-module-counts-as-sent into main 2026-10-09 15:03:36 +00: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 a6a9a818d2 Answer the review of hq issue 348's catalogue change
mesh/merge-gate pass: builds dnsmasq, docker, netcheck → ace, g14, novox, shanks; no bus step; 2 wait(s) for a person; every machine composes with the chan…
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
- The dnsmasq watch restarts only a unit that runs, without waiting for
  its start; a unit stopped, starting or failed is held and said (C2).
- dnsmasq_resolve asks only an address the resolver listens on (C3).
- Tests for the watch loop keeping its budget, a watch that wrote nothing
  or stopped looking, and a unit not running (C4).
- A restart is remembered as said only once emitted, restarts made before
  the bundle started are said, and whether one helped is said (C5).
- docker_events reads the runtime's whole buffer: complete when it is not
  full or reaches past the window, never a quiet machine read as lost (C6).
- netcheck: refused read with errors.Is; tests for addresses and ports,
  and for a container named as docker knows it.
2026-10-09 15:25:45 +02:00
jschoubben a0049f3e4e dnsmasq: bind each address once, at start, after it exists (hq issue 348)
bind-dynamic closes every listener whose interface a re-read of the
machine's addresses did not find, and a re-read during a bridge teardown
fails (dnsmasq 2.93 network.c:832-875, reproduced in a network namespace:
53 releases in 120 parallel teardowns). bind-interfaces never closes a
listener; a drop-in makes the unit wait for the private address and retry
a start that found it missing.
2026-10-09 15:04:45 +02:00
jochen 12de28c81d docker: read the resolver file a container really uses (hq issue 348)
On 2026-10-09 the control node's route-proxy failed every lookup against 10.10.0.1 alone for twenty
minutes while the machine's file listed both mesh resolvers; its copy lives under the runtime's
root-only directory and no tool read it. docker_resolv_conf reads /etc/resolv.conf as the container
sees it through the runtime's archive (docker cp, no exec), parses it, and says whether it lists the
same resolvers as the machine.
2026-10-09 15:04:45 +02:00
jochen 0f6986aead netcheck: ask one named resolver, and list what listens here (hq issue 348)
During the 2026-10-09 outage the control node's resolver stayed active while it no longer listened on
its private address, and nothing on the mesh could ask that address a question or show which sockets
were bound. netcheck_dns takes server (and tcp) to ask one resolver alone and say its response code;
netcheck_listening reads the kernel's socket tables, as ss -lunt does, with the owning program where
the tool's account may see it and a sentence where it may not.
2026-10-09 15:04:45 +02:00
jochen 3d8b52c9f4 docker: say where the runtime's event history begins, so a short history never reads as a quiet one (hq issue 348)
docker_events asked for three hours across the 2026-10-09 resolver outage on the control node answered
zero events: the runtime keeps only its last ~256 events in memory, and health-check execs filled them
with the last ten minutes. The answer now carries held, oldest_held and complete, and a history line
when the window reaches further back than what the runtime still holds. The type filter moves into the
tool, since the history's start is only known from all of it.
2026-10-09 15:04:45 +02:00
jschoubben 2b2397abe0 dnsmasq: ask the resolver a real question, and restart it when it stops answering (hq issue 348)
On 2026-10-09 the control node's dnsmasq stayed active and running, its
journal silent, and refused every question on its private address for
twenty minutes; nothing in the module looked past the unit.

The module is now Go (its TypeScript tools were never served): a root
watch asks the machine's own name on every listen address over UDP and
TCP every 15s, restarts the unit after two failing looks (2 min apart,
3 an hour), and says whether that helped. dnsmasq_health is the watch
process's declared check, so a restart that did not help becomes the
module's unhealthy condition; the service is judged by its unit.
2026-10-09 15:04:45 +02:00
mesh-admin 954e3e2087 Merge pull request 'nfs-server: check every share against where the backup seat keeps its data, on every pass (review follow-up of #136)' (#142) from fix/shares-review-followups into main 2026-10-09 12:55:40 +00:00
mesh-admin 26d6af3589 Merge pull request 'mounts: never leave a failed mount unit when a source is away, or the failure of a unit it takes over (hq ADR 0263 rules 4 and 6)' (#162) from fix/mounts-leaves-no-failed-unit into main 2026-10-09 12:55:38 +00:00
jochen 26f9c6e10d nfs-server: one test per guard (each stray guard, the failed-reload retry, the empty data list, the walk's stop, a stopping pass); say that a start drops hand-made exports and that unknown backup paths are a deliberate warning (review of #142)
mesh/merge-gate pass: builds nfs-server, sent nowhere; no bus step; 1 wait(s) for a person; every machine composes with the change as it did without (4 of …
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
2026-10-09 14:45:03 +02:00
jochen 7320d99a11 Make nfs-server reconcile the kernel's exports with its file, so a failed reload or a withdrawn grant is not left exported while the module says it is healthy; warn on a stale or unknown backup list 2026-10-09 14:43:32 +02:00
jochen 72a6d062dc Let SIGTERM cancel a running pass and bound nfs-server's clean-up at 10 s, so a hanging command cannot keep the export file past the stop timeout 2026-10-09 14:43:32 +02:00
jochen 87d6a5549c Check every share against where the backup seat keeps its data on every pass, so the hour a clean look is believed cannot hide a new repository 2026-10-09 14:43:32 +02:00
jochen 6d3cb73e3b mounts: gofmt the clearing tests
mesh/merge-gate pass: builds mounts, 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
2026-10-09 14:43:25 +02:00
jochen 02eab5aa8e mounts: test the clearing loop, the look's own clearing and a disk's presence; clear only a taken-over unit's earlier failure; clear and re-arm an automount that gave up; refuse a transient unit of the same name (review of #162) 2026-10-09 14:43:16 +02:00
jochen 0f330fc57d mounts: clear a failed mount of its own whose source was away, and the failure of a unit it takes over, so neither is said as the machine's own failed unit (hq ADR 0263 rules 4 and 6)
mesh/merge-gate pass: builds mounts, 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
2026-10-09 14:26:01 +02:00
mesh-admin 9e4267bf28 Merge pull request 'mesh-cli at go/v0.1.9, and the unannounced pull request's number (hq ADR 0272, issue 347)' (#160) from feat/272-the-tagged-sdk-and-the-pull-requests-number into main 2026-10-09 11:54:32 +00:00
mesh-admin a082615b8c Merge pull request 'gitea: the forge's tools never weaken protection or merge past the checks (hq ADR 0266 §7, route B)' (#153) from fix/the-forge-tools-never-weaken-protection into main 2026-10-09 11:49:43 +00:00
jochen 645961a87b mesh-cli at go/v0.1.9, and the unannounced pull request's number (hq ADR 0272, issue 347)
mesh/merge-gate pass: builds mesh-cli, 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
- mesh-cli pins the SDK at its tag now that go/v0.1.9 exists, instead of a
  pseudo-version of the branch it was cut from.
- mesh-delivery's stalled line for a head the forge never announced
  carries the pull request's number, which the controller's condition
  names in its headline.
2026-10-09 13:41:59 +02:00
jochen 1dca181e03 Put every repository path through the segment helper, gitea_open_pulls's included
mesh/merge-gate pass: builds gitea → 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 superseded: a newer delivery to the same trunk took over its walk
After merging #159 (issue 347): every /repos/<owner>/<name> the client builds
now takes its owner and name through one helper, whether they came from a
caller or from the forge's answer; the open-pulls fixture's shas are commits.
2026-10-09 13:41:35 +02:00
jochen 46fe6fff64 Merge remote-tracking branch 'origin/main' into fix/the-forge-tools-never-weaken-protection
# Conflicts:
#	modules/gitea/client.ts
2026-10-09 13:41:02 +02:00
mesh-admin ceab9a03b7 Merge pull request 'Announce a pull request opened after its push, and say one nobody checks (hq issue 347)' (#159) from fix/347-a-pull-request-opened-after-its-push-is-never-checked into main 2026-10-09 11:38:48 +00:00
mesh-admin 8ff8197a32 Merge pull request 'mesh-cli: the operator's command on every node, nox in zsh (hq ADR 0272)' (#156) from feat/272-mesh-cli into main 2026-10-09 11:38:44 +00:00
jochen 5eab586116 Put no caller's text in a forge path unjudged: every segment checked, every sent path checked again
mesh/merge-gate pass: builds gitea → 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 superseded: a newer head of the same pull request
The confirmation review of 2026-10-09 found the dedicated tools building paths
from caller text: a branch named .. became DELETE /repos/o/r/ (the repository),
a file path ../../../../version reached /api/v1/version, and a sha with
?sudo= set a status as any user. Each caller-supplied segment now goes through
one helper refusing empty, . and .. and / ? # %; a sha must be a commit; and
the client judges every path once more as it sends it: no dot or empty
segment, encoded or not, and only the allow-listed query keys.
2026-10-09 13:35:15 +02:00
jochen 32d55beb47 Say a pull request nobody checks as a delivery held, owned by mesh-delivery (hq issue 347)
mesh/merge-gate pass: builds gitea, 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 superseded: a newer delivery to the same trunk took over its walk
The confirmation review asked for the delivery domain's own vocabulary
instead of a provider standing. A gitea health check would need a `tools`
list (turning gitea's tools into seat verbs), a check of another kind
beside it, and would hold the forge's own deliveries at the first-node
gate; so the delivery's owner says it, with the word it already has.

- gitea_open_pulls (gitea): every open pull request from the forge itself,
  with what its base requires and the statuses its head has.
- mesh-delivery asks it every five minutes and keeps its own first-seen
  time per head (the forge's update time moves on every comment). A head
  on a base requiring mesh/merge-gate OR mesh/repo-check (hq requires only
  the second) with neither for ten minutes is a `stalled` line in state
  `unannounced`, which the controller raises as delivery.<id>.stalled (D14);
  H2 has nothing to close. Once announced it is a delivery of its own.
- The gitea-side watcher and its provider standing are gone.
2026-10-09 13:14:36 +02:00
jochen bcc6b3f52a gitea README: how to see a late-opened pull request announced (hq issue 347)
mesh/merge-gate pass: builds gitea → 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 superseded: a newer head of the same pull request
2026-10-09 12:58:13 +02:00
jochen 213950e334 Merge remote-tracking branch 'origin/main' into fix/the-forge-tools-never-weaken-protection
mesh/merge-gate pass: builds gitea → 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 superseded: a newer head of the same pull request
2026-10-09 12:57:16 +02:00
jochen ea772a9cce Let gitea_api send only the query keys it needs, never sudo, and never approve a pull request
The confirmation review of 2026-10-09 measured GET /user?sudo=… answer the
operator's own account: the query went to the forge unjudged, and the tools'
token is the site administrator's, so any caller could act as any user.
gitea_api now sends only allow-listed query keys and refuses sudo in any case,
and a review that approves or requests changes is the operator's.
2026-10-09 12:57:16 +02:00
jochen 67bce7e189 Announce a pull request opened after its push, and say one nobody checks (hq issue 347)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds gitea → 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)
The announcer read only the repositories whose update time moved since its
last pass, less a minute. Opening, reopening or retargeting a pull request
does not move a repository's update time, so a pull request opened more
than a minute after its push was never announced, never checked, and
nothing said so (mesh-sdk #14, 2026-10-09).

- Each pass also looks at every repository holding a pull request updated
  since the floor, by the forge's own search; every ten minutes, or when the
  search fails, at every repository.
- Every five minutes the forge is asked for every open pull request; one on
  a base requiring mesh/merge-gate whose head has had none for ten minutes
  is said as the forge failing that pull request's merge check (a provider
  standing, ADR 0224), which the controller keeps as a condition, and is
  recovered when a check arrives or it closes.
- The client refuses a pull request or issue number that is not one before
  asking the forge (a misnamed tool argument sent pulls/NaN).
2026-10-09 12:55:09 +02:00
mesh-admin e9f5727cad Merge pull request 'xorg: list the server's windows, and cut a window's screenshot from the screen (hq issue 345)' (#154) from fix/xorg-windows-and-window-screenshot into main 2026-10-09 10:51:01 +00:00
mesh-admin 1833045c33 Merge pull request 'picom: the backend and damage tracking are each machine's settings (hq issue 345)' (#157) from feat/picom-backend-and-damage-settings into main 2026-10-09 10:50:58 +00:00
mesh-admin 65308a7bce Merge pull request 'betterbird: a module for the mail and calendar client (hq issue 345)' (#155) from feat/betterbird-module into main 2026-10-09 10:50:54 +00:00
mesh-admin a783ac9478 Merge pull request 'xdg: keep the folders file trusted, so only the terminal sets the folders (hq issue 346)' (#158) from fix/xdg-user-dirs-trusted-again into main 2026-10-09 10:50:51 +00:00
jochen 6f1c559d93 mesh-cli: say an outcome not known, and hold the module to placing one file (review of hq ADR 0272)
mesh/merge-gate pass: builds claude-code, route-adapter, new: modules/mesh-cli → ace, g14, novox, shanks; no bus step; every machine composes with the chan…
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 line whose answer did not come in time is said as possibly run, with
its call, never as nothing having run. A test holds the module to one
archive resource unpacking one compiled Go bundle into /usr/local/bin.
The README says the MESH_VERB refusal is a courtesy and the controller is
the gate. SDK pinned at the go/cli branch's newest commit until go/v0.1.9.
2026-10-09 12:49:14 +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
jochen ff648ad32b Send the path gitea_api judged, keep a pull request's base, merge only into a guarded branch, and delete only the tools' own idle repositories
mesh/merge-gate pass: builds gitea → 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 superseded: a newer head of the same pull request
Follow-ups of the review of 2026-10-09: the escape hatch sent the raw path it
had judged in normalised form; a pull request's base could be moved through
it; the merge tool would merge into a branch no rule guards; a rule named for
a branch could undercut the glob rule guarding it; and the delete tool took
any unguarded repository. It now takes only a repository of the tools' own
user with no protection and no commit in 30 days, named in full.
2026-10-09 12:26:37 +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
jochen e1e043c088 Take out a mounts binary a local build left in this branch
mesh/merge-gate pass: builds claude-code, route-adapter, new: modules/mesh-cli → ace, g14, novox, shanks; no bus step; every machine composes with the chan…
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
2026-10-09 12:16:58 +02:00
jochen 2594a24b35 mesh-cli: the operator's command on every node, nox in zsh (hq ADR 0272)
mesh/merge-gate pass: builds claude-code, mounts, route-adapter, new: modules/mesh-cli → ace, g14, novox, shanks; no bus step; every machine composes with …
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
The records send the operator to "the controller's terminal", and on the
control-node nothing answered (hq issue 343). mesh-cli is a module of its
own, assigned to every node and built from its own directory: its program
is unpacked into /usr/local/bin by an archive resource (one file, no link,
no copy), and its zsh contribution makes nox its alias. It holds no
credential: it asks the node-engine on its machine over the SDK's go/cli
socket, and prints the controller's answer with its exit.

The agent module's refusals and README, and the route adapter's README, now
name `mesh-cli settings set …` on the control-node instead of a
mesh-controller command nobody could type; a settings file is passed inline,
since a path would be read where the controller runs.
2026-10-09 12:11:21 +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
jochen 080c113d69 Let the forge's tools never weaken protection or merge past the checks
mesh/merge-gate pass: builds gitea → 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 superseded: a newer head of the same pull request
gitea_api took any endpoint with a token minted for the forge's
administrator with write:admin: an agent could drop a branch's protection
or its required checks, set the merge check's status, write to main or
merge its own pull request (novox/hq ADR 0266 §7, the review of
2026-10-09). gitea_api now reads anything but the administration and
writes only what an author writes; protection is only strengthened; a
merge needs every required status a success and merges only that head; a
guarded repository is not deleted; and the token is minted without
write:admin, a kept one re-minted at the next start.
2026-10-09 11:47:36 +02:00
jochen eacda6e3ae claude-code: make the agent account at the home its node names
mesh/merge-gate fail: builds claude-code, claude-licence-manager, restic, sudo → ace, g14, novox, shanks; no bus step; 4 problem(s) the change brings; the …
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery superseded: a newer head of the same pull request
The controller fills ${machine:agent-home} in a user's home now, so an agent
account the operator names with a home of its own is created there, where the
module places its ~/.claude (hq ADR 0266). An existing account is never moved.
2026-10-09 11:02:52 +02:00
jochen 6d32975ca6 Refuse the agent-home gap reached through a module's own directory or a literal home
A path written as ${dir:<id>} can resolve under the agent's home, and a
literal /home path may be it; both escaped the check (hq ADR 0266).
2026-10-09 10:14:25 +02:00
jochen 87862f3bf5 Refuse an archive or a file written into under the agent's home
The node-engine writes those as root after checking the path, and the agent
account owns its home and could swap a link in between (hq ADR 0266). No
module does it today; this keeps a future one from opening the gap.
2026-10-09 10:14:25 +02:00
jochen 9afea75f93 sudo: give every command run through sudo a terminal of its own
An agent the operator starts as the agent account through sudo (hq ADR 0266)
would otherwise share the operator's terminal and could push keystrokes into
their shell after it ends. sudo still runs without a terminal for the mesh's
own tools, so nothing they do changes.
2026-10-09 10:14:25 +02:00
jochen 17b0ac9f6c claude-code: declare the account agents run as and the command that starts it
The control node is to run its agents under an account that cannot become root
(hq ADR 0266). Declare it as a user the node-engine judges root: never, put the
agent's directory in its home, and place claude-agent so the operator starts a
session there through their own sudo. Where no agent account is named, both
name the operator's account, as before.
2026-10-09 10:14:25 +02:00
jochen c8fb0016f2 Test that a sealed grant hides its words, not two letters
mesh/merge-gate pass: builds claude-code, claude-licence-manager, restic → ace, g14, novox, shanks; no bus step; every machine composes with the change as …
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 check looked for "rt" in a random base64 ciphertext, which holds it
about one time in seventy, and failed mesh-catalog #141's repo check by
chance.
2026-10-09 10:14:18 +02:00
jochen dae7bb848d restic: restore away from a directory another account owns
Root writes a restore beside the live data. In a directory another account
controls, such as the agent's home (hq ADR 0266), that account could put a
symbolic link where root writes and have root write anywhere; such a restore
now goes to the module's state.
2026-10-09 10:14:18 +02:00
jochen b57e6e8144 claude-code: write the agent's home as its own account where it has one
On the control node agents are to run as an account that cannot become root
(hq ADR 0266). The runtime keeps running as the operator, so the token, the
identity and the home-scope items go to the agent's home through a helper
started as that account; the licence and the account file follow the move,
what was placed in the operator's home goes once the agent holds the licence,
and the guard refuses every tool of a session running as anyone else,
recording to the journal. An API key is taken only from the agent's home and
only when it is one, here and in the licence manager, so the tool cannot carry
off the runtime's credential. Unchanged where no agent account is named.
2026-10-09 10:14:18 +02:00
mesh-admin 7339ce8556 Merge pull request 'claude-code: refuse registering what a session runs (hq issue 340)' (#149) from fix/340-what-a-session-runs-is-set-at-the-terminal into main 2026-10-09 08:00:24 +00:00
mesh-admin 73eef94101 Merge pull request 'mesh-delivery: end a merge that moves nothing or that a later merge delivered, and lift a group hold its check answered (hq issues 319, 316)' (#150) from fix/319-316-covered-and-lifted into main 2026-10-09 07:30:15 +00:00
mesh-admin 895e4a65b8 Merge pull request 'The catalogue's check type-checks and tests every touched TypeScript module' (#151) from check/typescript-modules-tested into main 2026-10-09 07:29:35 +00:00
jochen 7e510998d9 Merge branch 'check/typescript-modules-tested' into fix/319-316-covered-and-lifted
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
2026-10-09 03:29:00 +02:00
jochen 0d5227ee77 Test a TypeScript module whose tests alone changed
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered
A change to a module's tests moves no build (hq ADR 0267), so the planner's modules leave it out; the
TypeScript part also reads the modules the changed files are in.
2026-10-09 03:28:58 +02:00
jochen 0bd50fce0a The forge module's tool list test names the statuses and contains tools
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 superseded: a newer head of the same pull request
It had not been run before a merge: its expected list lacked gitea_commit_statuses since that tool came,
and now gitea_contains. The catalogue's TypeScript check, merged in from #151, runs it.
2026-10-09 03:23:19 +02:00
jochen 61ec4162d0 Merge branch 'check/typescript-modules-tested' into fix/319-316-covered-and-lifted 2026-10-09 03:23:05 +02:00
jochen f72903d05d A TypeScript module with tests and no sources of its own is tested, not skipped
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery superseded: a newer head of the same pull request
powerlevel10k carries only data and a test that reads it; the part skipped it silently.
2026-10-09 03:23:01 +02:00
jochen f072ca7297 Type-check and test every touched TypeScript module before it merges
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
The catalogue's check ran only Go. A TypeScript module's change, the forge's among them, ran nothing of
its own before it merged, and a type error would first have shown in its walk. A second part, declared
with mesh-check-also and run in the TypeScript toolchain, type-checks each touched TypeScript module with
the compiler and flags a build uses, against the SDK and Node types the toolchain holds, and runs its
tests as its own npm test does. A module that needs a package the toolchain lacks, or has no tests, is
said by name, never passed. The media catalogue's twin is hq issue 311.
2026-10-09 03:22:24 +02:00
mesh-admin 63de373b9d Merge pull request 'zfs: the safety review's fixes, and the module owns the pool's scrub schedule' (#147) from fix/zfs-safety-review into main 2026-10-09 01:02:48 +00:00
jochen 6327f297ea Cover a held merge only when the forge says a later delivered merge contains it (review of #150)
mesh/merge-gate pass: builds gitea, 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
mesh/delivery superseded: a newer head of the same pull request
Merge order on a protected trunk was the whole proof; the review asked for ancestry. The gitea module
gains gitea_contains, a read-only compare of two commits (total_commits 0 is contained), and mesh-delivery
asks it once per pair, off the lock like its plan ask, keeping the answer on the delivery. A forge that
cannot say leaves the merge held, not covered. Merge order stays the cheap first filter.

A later delivery now carries a module only when its own check passed (so a merge held without a passing
check is closed only by one that passed), its walk is of its own merge commit on the same trunk, and its
steps say the module reached every machine running it, not one. The words say what is known: a later
merge that contains it was delivered.
2026-10-09 02:57:18 +02:00
mesh-admin 6aabe8ee1f Merge pull request 'Issue 339: dunst's and xdg's files say nothing trusts their settings ("trusted": false)' (#148) from fix/339-files-nothing-trusts into main 2026-10-09 00:32:51 +00:00
jochen 2d1d4bf988 End a merge that moves nothing or that a later merge delivered, and lift a group hold its check answered (hq issues 319, 316)
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
mesh/delivery superseded: a newer head of the same pull request
A held delivery asked a person a question with no right answer: four hq merges that build nothing,
merges adding a module the controller sends nowhere, controller merges whose change a later walk of
the same trunk already delivered, and a group member held because its composed check had not yet
answered. Only stop could end them, which marks a delivered change as an error on its pull request.

- A merge whose plan moves no module (nothing, or only a new module sent nowhere) is delivered at once.
- A new final state, covered: every module a merge moves was sent by a finished walk of a later merge
  into the same trunk, which contains it. Shown as success on the pull request; a group counts it as
  delivered.
- A group member waits published while its composed check is asked and unanswered, and a member held
  for it is lifted back to published once it passes for the heads that merged.

Read again each tick, so the deliveries already held end by themselves after the rollout.
2026-10-09 02:24:26 +02:00
jochen a9acd3dd28 Keep the scheduled run's served slot in a folder of root's alone, written never through a link
mesh/merge-gate pass: builds zfs, 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
mesh/delivery delivered
Third review of #147. The run writes as root into /var/lib/zfs-scrub (0700, root's), apart from the
operator account's error counts in /var/lib/zfs-tools, and refuses a folder that is a link, open to
others or someone else's, and a file that is a link: a new file opened O_NOFOLLOW|O_EXCL, renamed over.
2026-10-09 01:55:54 +02:00
jochen bdd2d65404 Say that nothing trusts dunst's and xdg's settings, so a verb may still change them
mesh/merge-gate pass: builds dunst, 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
mesh/delivery delivered
With mesh-controller #170 a file that asks for a setting counts as trusted
unless it says otherwise, and its settings are set at the controller's
terminal alone (hq issue 339). These two are a person's own notifier look and
folder names in their home, which nothing else trusts.
2026-10-09 01:42:23 +02:00
jochen bd0182785c Run the scrub schedule as a scheduled process, never pause a scrub, and keep the pool's name the only setting in a unit
mesh/merge-gate pass: builds zfs, 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
mesh/delivery superseded: a newer head of the same pull request
The second review of #147 found three blockers before the home server.

1. ${setting:} was not filled in a unit's name, so the distribution timers would have reached the
   machine as "zfs-scrub-weekly@${setting:scrub-pool}.timer". mesh-controller #171 fills it and refuses
   any value that is not a unit-name part. The module's own schedule needs no setting in a name now: the
   node-engine's timer for the scheduled process zfs-mesh-scrub fires `zfs-tools scrub-run` every quarter
   of an hour, and the run reads the pool, cadence, day and time from the configuration file.
2. The run does what upstream's zfs-scrub@.service does: if a scrub is running it waits for it with
   zpool wait -t scrub, otherwise it starts one and waits. It never pauses or stops a scrub, and the unit
   has no ExecStop. A resilver defers the scrub. The served slot is kept in /var/lib/zfs-tools.
3. Unassigning left the pool with no schedule. mesh-host #66 (hq ADR 0269, proposed) gives the
   distribution timer back running and enabled. The README says not to unassign before it is merged.

Also: the run comes before the distribution timers in the declaration; the day and time are checked
against fixed sets and reach no unit file (an invalid value runs nothing); a due scrub not started an
hour after its time is a warning; the error counts outlive a restart in a small state file.
2026-10-09 01:40:02 +02:00
jochen e69506a26f Make the zfs module safe to put on the home server, and give it the pool's scrub schedule
mesh/merge-gate pass: builds zfs, 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
mesh/delivery superseded: a newer head of the same pull request
The safety review of the first version found it safe for the pool's data and asked for these fixes before
it goes on the home server:

- zfs_snapshot: no recursive. Every answer gives the dataset's free space and the pool's capacity, and
  says that the snapshot holds every block deleted or overwritten until it is destroyed by hand.
- A command that runs out of time is ended with its whole process group. An act that timed out is
  answered from the list, or as "outcome unknown", never as "NOT DONE".
- The watch runs as the operator account.
- The event follower reads ZFS's queue from its start, with a cursor per boot, so the pool's import and
  any downtime reach the journal once. It writes "dropped N events", kills zpool before waiting for it,
  and exits non-zero after five failed follows in a row.
- Past error counts are a warning, and counts that rise are unhealthy.
- The guard test catches .Run(, exec.Command(, exec.CommandContext( and process starts.
- The fixtures and the parser follow OpenZFS 2.4.4's own printers: human scan sizes under -p, no iostat
  section lines, error scrubs, per-vdev rebuild resilvers, paused scrubs.

The operator also asked the module to own the scrub. A timer and a service of the module's own run
zpool scrub -w -- <pool> from the scrub-pool, scrub-cadence, scrub-day and scrub-time settings. In the
same declaration, the distribution's weekly and monthly timers for that pool are held stopped and
disabled, so exactly one schedule scrubs it. zfs_scrub shows the schedule and the next run, with any
resilver's progress. zfs_scrub_start, zfs_scrub_pause and zfs_scrub_stop act with why. The health check
warns of a late or failed scrub and of a running resilver, and is unhealthy for a resilver that finished
with errors.
2026-10-09 01:17:34 +02:00
1991 changed files with 249207 additions and 14277 deletions
+18
View File
@@ -7,6 +7,24 @@ modules/jetbrains-toolbox/cmd/toolbox-tools/toolbox-tools
modules/messenger/cmd/messenger/messenger
modules/mesh-watcher/cmd/mesh-watcher/mesh-watcher
modules/claude-code/cmd/claude-code/claude-code
modules/dnsmasq/cmd/dnsmasq-tools/dnsmasq-tools
# ...and the same built from the module's root (go build ./cmd/<name>), which names it after the module.
modules/messenger/messenger
modules/claude-code/claude-code
modules/mesh-watcher/mesh-watcher
modules/dnsmasq/dnsmasq
modules/mesh-issues/cmd/mesh-issues/mesh-issues
modules/mounts/cmd/mounts/mounts
modules/mesh-knowledge/cmd/mesh-knowledge/mesh-knowledge
modules/mesh-knowledge/mesh-knowledge
modules/mesh-reads/mesh-reads
modules/mesh-reads/cmd/mesh-reads/mesh-reads
modules/mesh-files/mesh-files
modules/mesh-files/cmd/mesh-files/mesh-files
# The provisioner client built in place (issue 671).
modules/mesh-provisioner-client/cmd/mesh-provisioner-client/mesh-provisioner-client
modules/brother/cmd/brother/brother
modules/mesh-build-go/cmd/mesh-build-go/mesh-build-go
modules/mesh-config/mesh-config
modules/mesh-build-image/cmd/mesh-build-image/mesh-build-image
+6
View File
@@ -0,0 +1,6 @@
# Release 57: config and provisioning
This branch carries the work of hq design 57 (ADR 0348, ADR 0349) in this repository. Each package
merges here through its own reviewed pull request; the branch reaches main as one pull request, in one
delivery group with the branch of the same name in mesh-controller, mesh-host, mesh-sdk, mesh-catalog
and hq, after a rehearsal in the lab. This file is deleted by the last commit before that pull request.
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/agenthome
go 1.22
+128
View File
@@ -0,0 +1,128 @@
// Command agenthome refuses, in every module's manifest, a resource placed under the agent account's home
// that the node-engine cannot write without a gap (novox/hq ADR 0266).
//
// The agent account owns its home and everything in it, so it can put a symbolic or hard link anywhere
// there. The node-engine, as root, writes a whole file or a directory below a home through descriptors it
// opened itself and refuses a link it finds; it unpacks an archive, and writes into an existing file (`into`:
// json or block), only after checking the path, so a link swapped in between the check and the write would
// point root's write at any file. No module does either today. This check keeps it so: a resource of type
// `archive`, or a `file` with `into`, whose path is under ${machine:agent-home} — written so, reached through
// one of the module's own directories (${dir:<id>}), or written as a literal /home path — is refused. The
// node-engine refuses the same below the home of any account declared never to become root, whatever the path.
//
// go run . <catalogue root>
//
// Exits 1 on a finding.
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// AgentHome is the machine fact naming the agent account's home.
const AgentHome = "${machine:agent-home}"
// ofDir is where a manifest names one of its own directories: ${dir:<id>}.
var ofDir = regexp.MustCompile(`\$\{dir:([a-z0-9][a-z0-9_-]*)\}`)
// resolve replaces each ${dir:<id>} with that resource's own path, as far as the manifest says it, so a path
// reaching the agent's home through one of the module's directories is seen for what it is.
func resolve(p string, dirs map[string]string) string {
for i := 0; i < 8 && ofDir.MatchString(p); i++ {
p = ofDir.ReplaceAllStringFunc(p, func(word string) string {
if to, ok := dirs[ofDir.FindStringSubmatch(word)[1]]; ok {
return to
}
return word
})
}
return p
}
// underAHome says whether a resolved path lies under the agent's home: by its machine fact, or written as a
// literal home, which no definition may carry anyway (ADR 0112) and which may well be the agent's.
func underAHome(p string) bool {
// Cleaned first, so //home/x or /./home/x is seen as the /home/x it is.
if p != "" {
p = filepath.Clean(p)
}
return strings.HasPrefix(p, AgentHome) || strings.HasPrefix(p, "/home/") || p == "/home"
}
// Finding is one resource refused.
type Finding struct {
Module, ID, Why string
}
// Check reads every manifest under root/modules and answers what it refuses.
func Check(root string) ([]Finding, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
var out []Finding
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Resources []map[string]any `json:"resources"`
}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
dirs := map[string]string{}
for _, r := range m.Resources {
id, _ := r["id"].(string)
if p, _ := r["path"].(string); id != "" && p != "" {
dirs[id] = p
}
}
for _, r := range m.Resources {
p, _ := r["path"].(string)
if !underAHome(resolve(p, dirs)) {
continue
}
id, _ := r["id"].(string)
kind, _ := r["type"].(string)
into, _ := r["into"].(string)
switch {
case kind == "archive":
out = append(out, Finding{m.Module, id, "an archive unpacked under the agent's home"})
case kind == "file" && into != "":
out = append(out, Finding{m.Module, id, "a file written into (" + into + ") under the agent's home"})
}
}
}
return out, nil
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
found, err := Check(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
for _, f := range found {
fmt.Printf("%s: %s is %s: the node-engine writes it as root after checking the path, and the agent "+
"account could swap a link in between (novox/hq ADR 0266); place a whole file or a directory instead\n",
f.Module, f.ID, f.Why)
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("agent-home: no archive and no file written into under the agent's home")
}
+51
View File
@@ -0,0 +1,51 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func manifest(t *testing.T, root, module, resources string) {
t.Helper()
dir := filepath.Join(root, "modules", module)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
body := `{"module":"` + module + `","resources":[` + resources + `]}`
if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
func TestAnArchiveOrAFileWrittenIntoUnderTheAgentsHomeIsRefused(t *testing.T) {
root := t.TempDir()
manifest(t, root, "a", `{"id":"theme","type":"archive","path":"${machine:agent-home}/.local/share/x"}`)
manifest(t, root, "b", `{"id":"cfg","type":"file","path":"${machine:agent-home}/.claude.json","into":"json","content":"{}"}`)
manifest(t, root, "c", `{"id":"dir","type":"directory","path":"${machine:agent-home}/.claude"},`+
`{"id":"whole","type":"file","path":"${machine:agent-home}/x","content":"y"},`+
`{"id":"op","type":"archive","path":"${machine:account-home}/.local/share/x"}`)
manifest(t, root, "d", `{"id":"home","type":"directory","path":"${machine:agent-home}/.claude"},`+
`{"id":"inner","type":"directory","path":"${dir:home}/inner"},`+
`{"id":"via-dir","type":"archive","path":"${dir:inner}/theme"}`)
manifest(t, root, "e", `{"id":"literal","type":"file","path":"/home/agent/.bashrc","into":"block","content":"x"}`)
manifest(t, root, "f", `{"id":"unclean","type":"archive","path":"//./home/agent/x"}`)
found, err := Check(root)
if err != nil {
t.Fatal(err)
}
if len(found) != 5 || found[0].Module != "a" || found[1].Module != "b" || found[2].ID != "via-dir" ||
found[3].ID != "literal" || found[4].ID != "unclean" {
t.Fatalf("refused %+v", found)
}
}
func TestTheCatalogueHasNone(t *testing.T) {
found, err := Check("../..")
if err != nil {
t.Fatal(err)
}
if len(found) != 0 {
t.Fatalf("%+v", found)
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/config
go 1.22
+201
View File
@@ -0,0 +1,201 @@
// Command config refuses the settings form's names in a module that has moved to config (novox/hq ADR 0348
// clauses 1 and 11, design 57 package K6, issue 665).
//
// "Config" replaces "settings": a manifest declares its values under `config`, a mergeable file names the keys it
// takes under `takes-config`, and a file asks for a value with `${config:<key>}`. A mergeable file naming no keys at
// all is the settings form's last shape: it takes every key of its module's layer and declares none, so mesh-config
// could set none of them (ADR 0348 §10, ADR 0326 §3); it is found here like an old name (issue 735). The old names — `settings`,
// `takes-settings` and `${setting:<key>}` — stay accepted by the controller as aliases until every module has
// moved, and are then refused (ADR 0348 §1). The modules not yet moved are listed in the file `unmoved` beside this
// check, one per line: the catalogue's list ADR 0348 §11 names. Every other module says no old name, in its
// manifest or in a template file its manifest names; a module on the list that says none is taken off it; and a
// line the trunk's copy (the merge base's, given with -base) does not hold is refused, so the list only shrinks;
// while the trunk holds no copy, the modules in seed are the trunk's.
// When it is empty, the controller refuses the old names itself (design 57 package R1).
//
// Prose is not judged: a comment in a file's text that says "settings" composes into the machine's file, and
// rewording it would change what a node is sent.
//
// go run . [-base <the trunk's unmoved>] <catalogue root>
//
// Exits 1 on a finding.
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// The settings form's names (ADR 0348 §1). A key of a JSON object is a quoted string followed by a colon; a
// string value is never followed by one.
var (
settingRef = regexp.MustCompile(`\$\{setting:[a-z0-9][a-z0-9_.-]*\}`)
takesSettingsKey = regexp.MustCompile(`"takes-settings"\s*:`)
oldPlaceholder = "${setting:…}"
oldTakes = "takes-settings"
oldDeclaration = "settings"
wholeLayer = "taking its whole layer"
listFile = "unmoved"
listPathInCatalog = "checks/config/" + listFile
)
// Check answers, per module that says any, the settings form's names its manifest and its template files say.
func Check(root string) (map[string][]string, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
out := map[string][]string{}
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Settings json.RawMessage `json:"settings"`
Resources []map[string]any `json:"resources"`
}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
var said []string
if len(m.Settings) > 0 && string(m.Settings) != "null" {
said = append(said, oldDeclaration)
}
if takesSettingsKey.Match(raw) {
said = append(said, oldTakes)
}
if settingRef.Match(raw) {
said = append(said, oldPlaceholder)
}
for _, r := range m.Resources {
// A mergeable file naming no keys takes every key of the layer (ADR 0326 §3), declaring none of them.
if how, _ := r["merge"].(string); how != "" {
_, oldList := r["takes-settings"]
_, newList := r["takes-config"]
if !oldList && !newList {
said = append(said, fmt.Sprintf("%v %s", r["id"], wholeLayer))
}
}
name, _ := r["template"].(string)
if name == "" {
continue
}
text, err := os.ReadFile(filepath.Join(filepath.Dir(path), filepath.FromSlash(name)))
if err != nil {
return nil, fmt.Errorf("%s names the template %s: %w", m.Module, name, err)
}
if settingRef.Match(text) {
said = append(said, oldPlaceholder+" in "+name)
}
}
if len(said) > 0 {
sort.Strings(said)
out[m.Module] = said
}
}
return out, nil
}
// seed is the list as it was first written, standing for the trunk's copy until the trunk holds one.
// The modules taking a whole layer were found after the list was first written and before the trunk held a copy
// (issue 735), so they stand in it too.
var seed = map[string]bool{
"betterbird": true, "fail2ban": true, "influxdb": true, "keycloak": true, "mailu": true, "searxng": true,
"baserow": true, "confluence": true, "gitea": true, "gitlab": true, "grafana": true, "home-assistant": true,
"icecast": true, "jira": true, "letta": true, "matrix": true, "nodered": true, "supabase": true, "unifi": true,
}
// Unmoved is the modules a list names, one per line; a line starting with # and a blank line are not modules.
func Unmoved(path string) (map[string]bool, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
out := map[string]bool{}
s := bufio.NewScanner(f)
for s.Scan() {
line := strings.TrimSpace(s.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
out[line] = true
}
return out, s.Err()
}
// Judge answers the modules that say an old name and are not on the list (refused), the modules on the list that
// say none (stale), and the modules on the list that the trunk's list does not hold (added).
func Judge(found map[string][]string, unmoved, base map[string]bool) (refused, stale, added []string) {
for m := range found {
if !unmoved[m] {
refused = append(refused, m)
}
}
for m := range unmoved {
if _, says := found[m]; !says {
stale = append(stale, m)
}
if !base[m] {
added = append(added, m)
}
}
sort.Strings(refused)
sort.Strings(stale)
sort.Strings(added)
return refused, stale, added
}
func main() {
basePath := flag.String("base", "", "the trunk's copy of unmoved (the merge base's); seed when absent")
flag.Parse()
root := "."
if flag.NArg() > 0 {
root = flag.Arg(0)
}
found, err := Check(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
unmoved, err := Unmoved(listFile)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
base := seed
if *basePath != "" {
if base, err = Unmoved(*basePath); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
refused, stale, added := Judge(found, unmoved, base)
for _, m := range refused {
fmt.Printf("%s says %s, the settings form's names, and is not on %s: write config, takes-config and "+
"${config:<key>} throughout, and name the keys each mergeable file takes (novox/hq ADR 0348 §1, §11)\n", m, strings.Join(found[m], ", "), listPathInCatalog)
}
for _, m := range stale {
fmt.Printf("%s names %s, which says no old name: take it off the list\n", listPathInCatalog, m)
}
for _, m := range added {
fmt.Printf("%s gains %s, which the trunk's copy does not hold: the list only shrinks — move the module to "+
"config instead (novox/hq ADR 0348 §11)\n", listPathInCatalog, m)
}
if len(refused) > 0 || len(stale) > 0 || len(added) > 0 {
os.Exit(1)
}
fmt.Printf("config: no moved module says settings, takes-settings or ${setting:<key>}, or takes a whole layer "+
"(%d not yet moved)\n",
len(unmoved))
}
+131
View File
@@ -0,0 +1,131 @@
package main
import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
func module(t *testing.T, root, name, body string, files map[string]string) {
t.Helper()
dir := filepath.Join(root, "modules", name)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
for path, text := range files {
full := filepath.Join(dir, filepath.FromSlash(path))
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(full, []byte(text), 0o644); err != nil {
t.Fatal(err)
}
}
}
// Each of the settings form's names is found where it stands: the declaration, a mergeable file's keys, a
// placeholder in the manifest, and a placeholder in a template file the manifest names (novox/hq ADR 0348 §1).
func TestEveryOldNameIsFound(t *testing.T) {
root := t.TempDir()
module(t, root, "declares", `{"module":"declares","settings":{"size":{"kind":"preference","default":1,"why":"w"}},`+
`"resources":[{"id":"f","type":"file","content":"${setting:size}"}]}`, nil)
module(t, root, "takes", `{"module":"takes","resources":[{"id":"f","type":"file","merge":"json",`+
`"takes-settings":["a"],"content":"{}"}]}`, nil)
module(t, root, "asks", `{"module":"asks","serves":{"x":{"d":"${setting:domain}"}}}`, nil)
module(t, root, "templated", `{"module":"templated","config":{"size":{"type":"number","description":"d",`+
`"default":1}},"resources":[{"id":"f","type":"file","template":"files/conf"}]}`,
map[string]string{"files/conf": "size = ${setting:size}\n"})
module(t, root, "moved", `{"module":"moved","config":{"size":{"type":"number","description":"d","default":1}},`+
`"resources":[{"id":"f","type":"file","template":"files/conf"},{"id":"g","type":"file","merge":"json",`+
`"takes-config":[],"content":"{\"settings\": 1}"}]}`,
map[string]string{"files/conf": "size = ${config:size}\n# change the settings, never this file\n"})
module(t, root, "none", `{"module":"none","resources":[{"id":"f","type":"file","content":"no settings here"}]}`, nil)
got, err := Check(root)
if err != nil {
t.Fatal(err)
}
want := map[string][]string{
"asks": {"${setting:…}"},
"declares": {"${setting:…}", "settings"},
"takes": {"takes-settings"},
"templated": {"${setting:…} in files/conf"},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("found %v, want %v", got, want)
}
}
// A module not on the list says no old name; one on the list that says none is taken off it; a line the trunk's
// list does not hold is refused, so the list only shrinks until it is empty (novox/hq ADR 0348 §11).
func TestTheListOfModulesNotYetMovedOnlyShrinks(t *testing.T) {
found := map[string][]string{"old": {"settings"}, "forgotten": {"takes-settings"}}
unmoved := map[string]bool{"old": true, "done": true, "new": true}
base := map[string]bool{"old": true, "done": true, "forgotten": true}
refused, stale, added := Judge(found, unmoved, base)
if !reflect.DeepEqual(refused, []string{"forgotten"}) || !reflect.DeepEqual(stale, []string{"done", "new"}) ||
!reflect.DeepEqual(added, []string{"new"}) {
t.Fatalf("refused %v, stale %v, added %v", refused, stale, added)
}
}
func TestTheListIsReadWithoutItsComments(t *testing.T) {
path := filepath.Join(t.TempDir(), "unmoved")
if err := os.WriteFile(path, []byte("# why\n\nmailu\n searxng \n"), 0o644); err != nil {
t.Fatal(err)
}
got, err := Unmoved(path)
if err != nil || !reflect.DeepEqual(got, map[string]bool{"mailu": true, "searxng": true}) {
t.Fatalf("%v %v", got, err)
}
}
// The catalogue itself: every moved module says config alone, and the list names exactly the modules that
// still say settings.
func TestTheCatalogueSaysConfigInEveryMovedModule(t *testing.T) {
found, err := Check(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
unmoved, err := Unmoved("unmoved")
if err != nil {
t.Fatal(err)
}
refused, stale, added := Judge(found, unmoved, seed)
for _, m := range refused {
t.Errorf("%s is not on checks/config/unmoved and says %s", m, strings.Join(found[m], ", "))
}
for _, m := range stale {
t.Errorf("checks/config/unmoved names %s, which says no old name", m)
}
for _, m := range added {
t.Errorf("checks/config/unmoved gains %s, which its first copy did not hold", m)
}
}
// A mergeable file that names no keys takes every key of its module's layer (novox/hq ADR 0326 §3): the settings
// form's last shape, which declares none of what it takes, so mesh-config could set none of it (ADR 0348 §10). It
// is found like an old name, in a module that says no other (issue 735); one naming its keys, an empty list
// included, is not.
func TestAMergeableFileTakingTheWholeLayerIsFound(t *testing.T) {
root := t.TempDir()
module(t, root, "whole", `{"module":"whole","resources":[{"id":"runtime-config","type":"file","merge":"json",`+
`"template":"files/config.json"}]}`, map[string]string{"files/config.json": "{}\n"})
module(t, root, "names", `{"module":"names","config":{"url":{"type":"string","description":"d","default":""}},`+
`"resources":[{"id":"runtime-config","type":"file","merge":"json","takes-config":["url"],"content":"{}"}]}`, nil)
module(t, root, "none", `{"module":"none","resources":[{"id":"c","type":"file","merge":"json",`+
`"takes-config":[],"content":"{}"}]}`, nil)
module(t, root, "plain", `{"module":"plain","resources":[{"id":"c","type":"file","content":"{}"}]}`, nil)
got, err := Check(root)
if err != nil {
t.Fatal(err)
}
want := map[string][]string{"whole": {"runtime-config taking its whole layer"}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("found %v, want %v", got, want)
}
}
+5
View File
@@ -0,0 +1,5 @@
# The modules still written in the settings form (`settings`, `takes-settings`, `${setting:<key>}`, or a mergeable
# file naming no keys, which takes its whole layer), one per line: the catalogue's list of modules not yet moved to
# config (novox/hq ADR 0348 §11). Every other module says config alone. This list only shrinks: a module that says no
# old name is taken off, and a line the trunk's copy does not hold is refused. When it is empty, the controller
# refuses the old names (design 57 package R1).
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/deps
go 1.22
+255
View File
@@ -0,0 +1,255 @@
// Command deps makes a module's dependencies available to the catalogue's check, or says why it cannot
// (novox/hq issue 385).
//
// A module's tests run only when its dependencies can be fetched. The build seat clones what a check
// needs with the forge credential and forgets that credential before the check runs, so a module pinning
// an SDK commit the seat's module cache does not hold cannot fetch it from the forge. The SDK is cloned
// beside the check all the same (MESH_CHECK_BESIDE, mesh-controller's besideRefs): every requirement of the
// mesh's own forge is taken from the clone of its repository there, at the commit or the tag its version
// names, through a go.mod of its own (-modfile) — the module's own files are not changed, and no forge
// credential is added.
//
// Usage: deps -beside <dir> -work <dir> <module dir>. It prints the go.mod to build the module with, or
// nothing when the module fetches what it needs; when it cannot, it prints why and exits 1, and the check
// fails: a module whose tests could not run is a failure, never a note.
package main
import (
"archive/tar"
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
)
// forge is the prefix of the Go modules of the mesh's own forge: <forge><repository>[/<directory>].
const forge = "git.novox.be/novox/"
func main() {
beside := flag.String("beside", "", "where the repositories beside the check are cloned (MESH_CHECK_BESIDE)")
work := flag.String("work", os.TempDir(), "where the go.mod and the sources taken from beside are written")
flag.Parse()
if flag.NArg() != 1 {
fmt.Println("usage: deps -beside <dir> -work <dir> <module dir>")
os.Exit(2)
}
modfile, err := resolve(flag.Arg(0), *beside, *work)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if modfile != "" {
fmt.Println(modfile)
}
}
// resolve is the go.mod the module at dir builds with: "" when its own fetches everything, else one
// beside it in work that takes every requirement of the mesh's forge from its clone beside the check.
func resolve(dir, beside, work string) (string, error) {
out, err := goIn(dir, "mod", "download")
if err == nil {
return "", nil
}
fetching := lastLine(out)
required, err := forgeRequirements(dir)
if err != nil {
return "", err
}
if len(required) == 0 {
return "", fmt.Errorf("its dependencies cannot be fetched: %s", fetching)
}
if beside == "" {
return "", fmt.Errorf("its dependencies cannot be fetched (%s), and nothing was cloned beside the check to "+
"take %s from", fetching, repositories(required))
}
into, err := os.MkdirTemp(work, filepath.Base(dir)+"-")
if err != nil {
return "", err
}
modfile := filepath.Join(into, "go.mod")
if err := copyFile(filepath.Join(dir, "go.mod"), modfile); err != nil {
return "", err
}
if err := copyFile(filepath.Join(dir, "go.sum"), filepath.Join(into, "go.sum")); err != nil &&
!errors.Is(err, os.ErrNotExist) {
return "", err
}
for _, r := range required {
source, err := export(beside, r, into)
if err != nil {
return "", fmt.Errorf("its dependencies cannot be fetched (%s), and %s@%s cannot be taken from the "+
"clone beside the check: %w", fetching, r.Path, r.Version, err)
}
if out, err := goIn(dir, "mod", "edit", "-replace", r.Path+"@"+r.Version+"="+source, modfile); err != nil {
return "", fmt.Errorf("%s@%s cannot be taken from the clone beside the check: %s", r.Path, r.Version,
lastLine(out))
}
}
if out, err := goIn(dir, "mod", "download", "-modfile="+modfile); err != nil {
return "", fmt.Errorf("its dependencies cannot be fetched even with %s taken from the clone beside the "+
"check: %s", repositories(required), lastLine(out))
}
return modfile, nil
}
// requirement is one module the module requires.
type requirement struct {
Path string
Version string
}
// forgeRequirements is every requirement of the module at dir that comes from the mesh's own forge.
func forgeRequirements(dir string) ([]requirement, error) {
out, err := goIn(dir, "mod", "edit", "-json")
if err != nil {
return nil, fmt.Errorf("its go.mod cannot be read: %s", lastLine(out))
}
var mod struct{ Require []requirement }
if err := json.Unmarshal(out, &mod); err != nil {
return nil, fmt.Errorf("its go.mod cannot be read: %w", err)
}
var on []requirement
for _, r := range mod.Require {
if strings.HasPrefix(r.Path, forge) {
on = append(on, r)
}
}
return on, nil
}
// repositoryOf splits a module path of the forge into its repository and the directory in it.
func repositoryOf(path string) (string, string) {
repo, sub, _ := strings.Cut(strings.TrimPrefix(path, forge), "/")
return repo, sub
}
func repositories(rs []requirement) string {
var names []string
for _, r := range rs {
repo, _ := repositoryOf(r.Path)
names = append(names, repo)
}
return strings.Join(names, ", ")
}
// pseudo is a pseudo-version's commit: v0.1.14-0.20261012082113-9747f3e1b1ac → 9747f3e1b1ac.
var pseudo = regexp.MustCompile(`[-.]\d{14}-([0-9a-f]{12})$`)
// refOf is the ref a version names in its repository: a pseudo-version's commit, else the tag a module in
// a directory is released under (<directory>/<version>).
func refOf(sub, version string) string {
version = strings.TrimSuffix(version, "+incompatible")
if m := pseudo.FindStringSubmatch(version); m != nil {
return m[1]
}
if sub != "" {
return sub + "/" + version
}
return version
}
// export writes the module r names, at its version, from its repository's clone beside the check into
// into, and answers its directory.
func export(beside string, r requirement, into string) (string, error) {
repo, sub := repositoryOf(r.Path)
clone := filepath.Join(beside, repo)
if _, err := os.Stat(filepath.Join(clone, ".git")); err != nil {
return "", fmt.Errorf("no clone of %s is beside the check", repo)
}
ref := refOf(sub, r.Version)
commit, err := exec.Command("git", "-C", clone, "rev-parse", "--verify", "--quiet", ref+"^{commit}").Output()
if err != nil {
return "", fmt.Errorf("the clone of %s holds no %s (a commit or tag not pushed to the forge?)", repo, ref)
}
at := strings.TrimSpace(string(commit))
args := []string{"-C", clone, "archive", "--format=tar", at}
if sub != "" {
args = append(args, "--", sub)
}
var stderr bytes.Buffer
archive := exec.Command("git", args...)
archive.Stderr = &stderr
raw, err := archive.Output()
if err != nil {
return "", fmt.Errorf("git archive of %s at %s: %v %s", repo, ref, err, strings.TrimSpace(stderr.String()))
}
root := filepath.Join(into, repo+"@"+at[:12])
if err := untar(raw, root); err != nil {
return "", err
}
source := filepath.Join(root, filepath.FromSlash(sub))
if _, err := os.Stat(filepath.Join(source, "go.mod")); err != nil {
return "", fmt.Errorf("%s at %s has no go.mod in %q", repo, ref, sub)
}
return source, nil
}
// untar writes a tar's directories and regular files under root.
func untar(raw []byte, root string) error {
r := tar.NewReader(bytes.NewReader(raw))
for {
h, err := r.Next()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
name := filepath.Clean(filepath.FromSlash(h.Name))
if name == "." || strings.HasPrefix(name, "..") || filepath.IsAbs(name) {
continue
}
path := filepath.Join(root, name)
switch h.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(path, 0o755); err != nil {
return err
}
case tar.TypeReg:
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
body, err := io.ReadAll(r)
if err != nil {
return err
}
if err := os.WriteFile(path, body, 0o644); err != nil {
return err
}
}
}
}
func copyFile(from, to string) error {
raw, err := os.ReadFile(from)
if err != nil {
return err
}
return os.WriteFile(to, raw, 0o644)
}
// goIn runs the go command in dir.
func goIn(dir string, args ...string) ([]byte, error) {
cmd := exec.Command("go", args...)
cmd.Dir = dir
var stderr bytes.Buffer
cmd.Stderr = &stderr
out, err := cmd.Output()
if err != nil {
return append(out, stderr.Bytes()...), err
}
return out, nil
}
// lastLine is the last line a command said, which is where go says what failed.
func lastLine(out []byte) string {
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
return strings.TrimSpace(lines[len(lines)-1])
}
+177
View File
@@ -0,0 +1,177 @@
package main
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// offline is a Go that fetches nothing: what the build seat is once the forge credential is gone, for a pin
// its module cache does not hold (novox/hq issue 385).
func offline(t *testing.T) {
t.Helper()
t.Setenv("GOPROXY", "off")
t.Setenv("GOFLAGS", "")
t.Setenv("GOWORK", "off")
// Not GOPRIVATE: a private module is fetched directly by git, around GOPROXY, with whatever credential
// the machine running the test holds.
t.Setenv("GOPRIVATE", "")
t.Setenv("GONOPROXY", "")
t.Setenv("GOSUMDB", "off")
t.Setenv("GOMODCACHE", t.TempDir())
}
func git(t *testing.T, dir string, args ...string) string {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t", "GIT_COMMITTER_NAME=t",
"GIT_COMMITTER_EMAIL=t@t", "GIT_AUTHOR_DATE=2026-10-12T08:21:13Z", "GIT_COMMITTER_DATE=2026-10-12T08:21:13Z")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out)
}
return strings.TrimSpace(string(out))
}
func write(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
// sdkBeside is the SDK's repository cloned beside a check, its Go module under go/, one commit tagged
// go/v0.1.7. It answers the beside root and the commit.
func sdkBeside(t *testing.T) (string, string) {
t.Helper()
beside := t.TempDir()
repo := filepath.Join(beside, "mesh-sdk")
write(t, filepath.Join(repo, "go", "go.mod"), "module git.novox.be/novox/mesh-sdk/go\n\ngo 1.22\n")
write(t, filepath.Join(repo, "go", "sdk", "sdk.go"), "package sdk\n\nfunc Hello() string { return \"hello\" }\n")
write(t, filepath.Join(repo, "README.md"), "the SDK\n")
git(t, repo, "init", "--quiet")
git(t, repo, "add", ".")
git(t, repo, "commit", "--quiet", "-m", "sdk")
git(t, repo, "tag", "go/v0.1.7")
return beside, git(t, repo, "rev-parse", "HEAD")
}
// moduleRequiring is a catalogue module whose code and test use the SDK at the version given.
func moduleRequiring(t *testing.T, version string) string {
t.Helper()
dir := filepath.Join(t.TempDir(), "messenger")
write(t, filepath.Join(dir, "go.mod"), "module git.novox.be/novox/mesh-catalog/modules/messenger\n\ngo 1.22\n\n"+
"require git.novox.be/novox/mesh-sdk/go "+version+"\n")
write(t, filepath.Join(dir, "main.go"), "package main\n\nimport \"git.novox.be/novox/mesh-sdk/go/sdk\"\n\n"+
"func main() { println(sdk.Hello()) }\n")
return dir
}
func builds(t *testing.T, dir, modfile string) {
t.Helper()
args := []string{"vet"}
if modfile != "" {
args = append(args, "-modfile="+modfile)
}
cmd := exec.Command("go", append(args, "./...")...)
cmd.Dir = dir
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("the module does not build with %q: %v\n%s", modfile, err, out)
}
}
// A module that needs nothing it cannot fetch is left as it is: no other go.mod.
func TestAModuleThatFetchesItsDependenciesNeedsNothingBeside(t *testing.T) {
offline(t)
dir := filepath.Join(t.TempDir(), "plain")
write(t, filepath.Join(dir, "go.mod"), "module example.com/plain\n\ngo 1.22\n")
write(t, filepath.Join(dir, "main.go"), "package main\n\nfunc main() {}\n")
modfile, err := resolve(dir, "", t.TempDir())
if err != nil || modfile != "" {
t.Fatalf("resolve = %q, %v; want nothing", modfile, err)
}
}
// The case of issue 385: the module pins an SDK commit the build seat cannot fetch. It is taken from the
// SDK's clone beside the check, and the module builds against it.
func TestAPinnedSDKCommitIsTakenFromTheCloneBeside(t *testing.T) {
offline(t)
beside, commit := sdkBeside(t)
dir := moduleRequiring(t, "v0.1.14-0.20261012082113-"+commit[:12])
modfile, err := resolve(dir, beside, t.TempDir())
if err != nil {
t.Fatalf("resolve: %v", err)
}
if modfile == "" {
t.Fatal("resolve said the module fetches its dependencies, with nothing to fetch them from")
}
builds(t, dir, modfile)
if raw, _ := os.ReadFile(filepath.Join(dir, "go.mod")); strings.Contains(string(raw), "replace") {
t.Fatalf("the module's own go.mod was changed:\n%s", raw)
}
}
// A released SDK is found by the tag it is released under: go/<version>.
func TestATaggedSDKIsTakenFromTheCloneBeside(t *testing.T) {
offline(t)
beside, _ := sdkBeside(t)
dir := moduleRequiring(t, "v0.1.7")
modfile, err := resolve(dir, beside, t.TempDir())
if err != nil {
t.Fatalf("resolve: %v", err)
}
builds(t, dir, modfile)
}
// A commit the clone beside does not hold (a branch never pushed) cannot be tested, and says which.
func TestACommitTheCloneBesideDoesNotHoldIsSaid(t *testing.T) {
offline(t)
beside, _ := sdkBeside(t)
dir := moduleRequiring(t, "v0.1.14-0.20261010132341-4f3efc98e0c0")
_, err := resolve(dir, beside, t.TempDir())
if err == nil {
t.Fatal("resolve found a commit nothing holds")
}
for _, want := range []string{"git.novox.be/novox/mesh-sdk/go@v0.1.14-0.20261010132341-4f3efc98e0c0", "4f3efc98e0c0"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("the reason %q does not name %s", err, want)
}
}
}
// With nothing cloned beside (a check by hand without the forge), the reason says that.
func TestNoCloneBesideIsSaid(t *testing.T) {
offline(t)
_, commit := sdkBeside(t)
dir := moduleRequiring(t, "v0.1.14-0.20261012082113-"+commit[:12])
for _, beside := range []string{"", t.TempDir()} {
_, err := resolve(dir, beside, t.TempDir())
if err == nil {
t.Fatalf("resolve with %q beside found what is not there", beside)
}
if !strings.Contains(err.Error(), "mesh-sdk") {
t.Errorf("the reason %q does not name the repository it needed", err)
}
}
}
func TestTheRefOfAVersion(t *testing.T) {
cases := []struct{ sub, version, want string }{
{"go", "v0.1.14-0.20261012082113-9747f3e1b1ac", "9747f3e1b1ac"},
{"go", "v0.0.0-20261012082113-9747f3e1b1ac", "9747f3e1b1ac"},
{"go", "v0.1.7", "go/v0.1.7"},
{"", "v1.2.0", "v1.2.0"},
{"", "v2.0.0+incompatible", "v2.0.0"},
}
for _, c := range cases {
if got := refOf(c.sub, c.version); got != c.want {
t.Errorf("refOf(%q, %q) = %q, want %q", c.sub, c.version, got, c.want)
}
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/imagebuild
go 1.22
+92
View File
@@ -0,0 +1,92 @@
// Command imagebuild refuses any manifest but modules/buildah providing the rootless-image-build provision
// (novox/hq ADR 0487 §3, issue 1704): the image builder's tool is buildah alone, and docker, whose daemon holds
// the machine's root, never stands in for it. A second provider would let a node compose mesh-build-image over
// something that is not the rootless builder it was designed against.
//
// go run . <catalogue root>
//
// Exits 1 on a finding.
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
)
// Provision is the provision only Provider may provide.
const Provision = "rootless-image-build"
// Provider is the one module allowed to provide it.
const Provider = "buildah"
// Finding is one manifest refused: a module other than buildah providing the provision.
type Finding struct {
Module string
}
// Check reads every manifest under root/modules and answers which ones it refuses, and whether buildah provides.
func Check(root string) ([]Finding, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
var out []Finding
provided := false
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Provides []struct {
Name string `json:"name"`
} `json:"provides"`
}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
for _, p := range m.Provides {
if p.Name != Provision {
continue
}
if m.Module == Provider {
provided = true
} else {
out = append(out, Finding{Module: m.Module})
}
break
}
}
if !provided {
out = append(out, Finding{Module: Provider})
}
return out, nil
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
found, err := Check(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
for _, f := range found {
if f.Module == Provider {
fmt.Printf("%s does not provide %s (novox/hq ADR 0487 §3)\n", Provider, Provision)
continue
}
fmt.Printf("%s provides %s: only %s may, and docker never (novox/hq ADR 0487 §3)\n", f.Module, Provision, Provider)
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("imagebuild: rootless-image-build is provided only by buildah")
}
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"os"
"path/filepath"
"reflect"
"testing"
)
func catalogue(t *testing.T, manifests map[string]string) string {
t.Helper()
root := t.TempDir()
for name, body := range manifests {
dir := filepath.Join(root, "modules", name)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
return root
}
const buildah = `{"module":"buildah","provides":[{"name":"rootless-image-build","reach":"machine"}]}`
func TestOnlyBuildahProvidesTheRootlessImageBuild(t *testing.T) {
root := catalogue(t, map[string]string{"buildah": buildah, "go": `{"module":"go","provides":[{"name":"go-toolchain"}]}`})
got, err := Check(root)
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Errorf("found %v, want none", got)
}
}
func TestDockerAndAnySecondProviderAreRefused(t *testing.T) {
root := catalogue(t, map[string]string{
"buildah": buildah,
"docker": `{"module":"docker","provides":[{"name":"rootless-image-build"}]}`,
"other": `{"module":"other","provides":[{"name":"x"},{"name":"rootless-image-build"}]}`,
})
got, err := Check(root)
if err != nil {
t.Fatal(err)
}
want := []Finding{{Module: "docker"}, {Module: "other"}}
if !reflect.DeepEqual(got, want) {
t.Errorf("found %v, want %v", got, want)
}
}
func TestBuildahMustProvideIt(t *testing.T) {
root := catalogue(t, map[string]string{"buildah": `{"module":"buildah"}`})
got, err := Check(root)
if err != nil {
t.Fatal(err)
}
if want := []Finding{{Module: "buildah"}}; !reflect.DeepEqual(got, want) {
t.Errorf("found %v, want %v", got, want)
}
}
// The catalogue as it stands: this is the check itself, run here too.
func TestTheCatalogueGivesTheProvisionToBuildahAlone(t *testing.T) {
got, err := Check("../..")
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Errorf("the catalogue's providers of rootless-image-build are wrong: %v", got)
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/imageplace
go 1.22
+91
View File
@@ -0,0 +1,91 @@
// Command imageplace refuses an image artifact whose Dockerfile is not a file inside its own module's directory
// (novox/hq ADR 0487 §1, issue 1707): the image builder builds a module from that module's directory alone, so
// a `from` that leaves it (../other/Dockerfile) or names a file that is not there could never build.
//
// go run . <catalogue root>
//
// Exits 1 on a finding.
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
// Finding is one image refused: Module's artifact Artifact names From, and Why says what is wrong with it.
type Finding struct {
Module, Artifact, From, Why string
}
// Check reads every manifest under root/modules and answers which images it refuses.
func Check(root string) ([]Finding, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
var out []Finding
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Build struct {
Artifacts []struct {
Name string `json:"name"`
Kind string `json:"kind"`
From string `json:"from"`
} `json:"artifacts"`
} `json:"build"`
}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
dir := filepath.Dir(path)
for _, a := range m.Build.Artifacts {
if a.Kind != "image" {
continue
}
f := Finding{Module: m.Module, Artifact: a.Name, From: a.From}
clean := filepath.Clean(a.From)
switch {
case a.From == "" || filepath.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)):
f.Why = "is not a path inside the module's own directory"
default:
info, err := os.Stat(filepath.Join(dir, clean))
if err != nil || info.IsDir() {
f.Why = "is not a file in the module's directory"
}
}
if f.Why != "" {
out = append(out, f)
}
}
}
return out, nil
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
found, err := Check(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
for _, f := range found {
fmt.Printf("%s: image %s builds from %q, which %s (novox/hq ADR 0487 §1)\n", f.Module, f.Artifact, f.From, f.Why)
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("imageplace: every image builds from a Dockerfile in its own module's directory")
}
+84
View File
@@ -0,0 +1,84 @@
package main
import (
"os"
"path/filepath"
"reflect"
"testing"
)
// module writes modules/<name>/module.json with one image artifact from `from`, and the files named.
func module(t *testing.T, root, name, from string, files ...string) {
t.Helper()
dir := filepath.Join(root, "modules", name)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
body := `{"module":"` + name + `","build":{"artifacts":[{"name":"server","kind":"image","from":"` + from + `"},{"name":"files","kind":"archive","from":"../elsewhere"}]}}`
if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
for _, f := range files {
p := filepath.Join(dir, f)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte("FROM scratch\n"), 0o644); err != nil {
t.Fatal(err)
}
}
}
func TestADockerfileInTheModulesOwnDirectoryPasses(t *testing.T) {
root := t.TempDir()
module(t, root, "own", "Dockerfile", "Dockerfile")
module(t, root, "nested", "image/Dockerfile", "image/Dockerfile")
got, err := Check(root)
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Errorf("found %v, want none", got)
}
}
func TestADockerfileOutsideTheModulesDirectoryFails(t *testing.T) {
root := t.TempDir()
module(t, root, "other", "Dockerfile", "Dockerfile")
module(t, root, "leaves", "../other/Dockerfile")
module(t, root, "absolute", "/etc/Dockerfile")
got, err := Check(root)
if err != nil {
t.Fatal(err)
}
var mods []string
for _, f := range got {
mods = append(mods, f.Module)
}
if want := []string{"absolute", "leaves"}; !reflect.DeepEqual(mods, want) {
t.Errorf("refused %v (%v), want %v", mods, got, want)
}
}
func TestAMissingDockerfileFails(t *testing.T) {
root := t.TempDir()
module(t, root, "gone", "Dockerfile")
got, err := Check(root)
if err != nil {
t.Fatal(err)
}
if len(got) != 1 || got[0].Module != "gone" {
t.Errorf("found %v, want gone refused", got)
}
}
// The catalogue as it stands: this is the check itself, run here too.
func TestTheCataloguesImagesBuildFromTheirOwnDirectories(t *testing.T) {
got, err := Check("../..")
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Errorf("an image builds from outside its module: %v", got)
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/mailstore
go 1.22
+81
View File
@@ -0,0 +1,81 @@
// Command mailstore refuses any manifest but modules/mesh-mail claiming the mail-store seat (novox/hq ADR 0378
// §3, issue 800 part A): the operator's mail is read, searched and sent through this one seat, and a second
// claimant would be a second path to the same mail that ADR 0378 never accounted for.
//
// go run . <catalogue root>
//
// Exits 1 on a finding.
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
)
// MailStoreSeat is the seat only mesh-mail may claim.
const MailStoreSeat = "mail-store"
// MailStoreModule is the one module allowed to claim it.
const MailStoreModule = "mesh-mail"
// Finding is one manifest refused: a module other than mesh-mail claiming mail-store.
type Finding struct {
Module string
}
// Check reads every manifest under root/modules and answers which ones it refuses.
func Check(root string) ([]Finding, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
var out []Finding
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Claims []struct {
Name string `json:"name"`
} `json:"claims"`
}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
if m.Module == MailStoreModule {
continue
}
for _, c := range m.Claims {
if c.Name == MailStoreSeat {
out = append(out, Finding{Module: m.Module})
break
}
}
}
return out, nil
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
found, err := Check(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
for _, f := range found {
fmt.Printf("%s claims the mail-store seat: only %s may (novox/hq ADR 0378 §3)\n", f.Module, MailStoreModule)
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("mail-store: claimed only by mesh-mail")
}
+43
View File
@@ -0,0 +1,43 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func manifest(t *testing.T, root, module, claims string) {
t.Helper()
dir := filepath.Join(root, "modules", module)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
body := `{"module":"` + module + `","claims":[` + claims + `]}`
if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
func TestAnotherModuleClaimingMailStoreIsRefused(t *testing.T) {
root := t.TempDir()
manifest(t, root, "mesh-mail", `{"name":"mail-store","scope":"mesh"}`)
manifest(t, root, "betterbird", `{"name":"mail-store","scope":"mesh"}`)
manifest(t, root, "dunst", `{"name":"node-notifier","scope":"node"}`)
found, err := Check(root)
if err != nil {
t.Fatal(err)
}
if len(found) != 1 || found[0].Module != "betterbird" {
t.Fatalf("refused %+v", found)
}
}
func TestTheCatalogueHasNone(t *testing.T) {
found, err := Check("../..")
if err != nil {
t.Fatal(err)
}
if len(found) != 0 {
t.Fatalf("%+v", found)
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/owned
go 1.22
+242
View File
@@ -0,0 +1,242 @@
// Command owned judges how the catalogue's directories and archives treat files they did not write (novox/hq
// ADR 0422 §8, issue 1024). It refuses, naming the module, the resource and the reason:
//
// 1. a directory or archive without `unknown`, or with a value but move-out, module or program;
//
// 2. `move-out` on a path not under ${machine:account-home} or ${machine:agent-home};
//
// 3. `knows` with an empty, absolute, `.`/`..`/empty-component or repeated path;
//
// 4. a path one module declares (a resource's path, or a fact with "home": true) lying inside a directory of
// another module, after ${dir:<id>} is resolved within each module and both homes are read as one root.
// Archives are not containers for this rule. This holds for ~/.ssh what checks/sshdir held (ADR 0415 §6).
//
// go run . <catalogue root>
//
// Exits 1 on a finding.
package main
import (
"encoding/json"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
)
// Home is the one root both machine homes are read as.
const Home = "${home}"
var (
ofDir = regexp.MustCompile(`\$\{dir:([a-z0-9][a-z0-9_-]*)\}`)
homeVar = strings.NewReplacer("${machine:account-home}", Home, "${machine:agent-home}", Home)
values = map[string]bool{"move-out": true, "module": true, "program": true}
)
// Finding is one refusal.
type Finding struct {
Rule int
Module, ID, Why string
}
type resource struct {
ID, Type, Path, Unknown string
Knows []string
HasKnows bool
}
type parsed struct {
Module string
Resources []resource
Facts []string // paths of facts with "home": true, under the home
}
func load(file string) (*parsed, error) {
raw, err := os.ReadFile(file)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Resources []struct {
ID string `json:"id"`
Type string `json:"type"`
Path string `json:"path"`
Unknown *string `json:"unknown"`
Knows []string `json:"knows"`
} `json:"resources"`
Facts map[string]struct {
Path string `json:"path"`
Home bool `json:"home"`
} `json:"facts"`
}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", file, err)
}
out := &parsed{Module: m.Module}
if out.Module == "" {
out.Module = filepath.Base(filepath.Dir(file))
}
for _, r := range m.Resources {
x := resource{ID: r.ID, Type: r.Type, Path: r.Path, Knows: r.Knows, HasKnows: r.Knows != nil}
if r.Unknown != nil {
x.Unknown = *r.Unknown
if x.Unknown == "" {
x.Unknown = "(empty)"
}
}
out.Resources = append(out.Resources, x)
}
keys := make([]string, 0, len(m.Facts))
for k := range m.Facts {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if f := m.Facts[k]; f.Home && f.Path != "" {
out.Facts = append(out.Facts, f.Path)
}
}
return out, nil
}
// norm resolves ${dir:<id>} from the module's own resources, reads both homes as one root and cleans the path.
func norm(p string, dirs map[string]string) string {
for i := 0; i < 8 && ofDir.MatchString(p); i++ {
p = ofDir.ReplaceAllStringFunc(p, func(w string) string {
if to, ok := dirs[ofDir.FindStringSubmatch(w)[1]]; ok {
return to
}
return w
})
}
p = homeVar.Replace(p)
if p == "" {
return ""
}
return path.Clean(p)
}
func underHome(p string) bool { return strings.HasPrefix(p, Home+"/") }
// badKnows says why a knows entry is refused, or "".
func badKnows(k string) string {
if k == "" {
return "is empty"
}
if strings.HasPrefix(k, "/") {
return "is absolute"
}
for _, c := range strings.Split(strings.TrimSuffix(k, "/"), "/") {
if c == "" || c == "." || c == ".." {
return "has an empty, . or .. component"
}
}
return ""
}
type declared struct {
module, id, path string
}
// Check reads every manifest under root/modules and answers what it refuses.
func Check(root string) ([]Finding, error) {
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(files)
var out []Finding
var paths []declared
type container struct{ module, path string }
var containers []container
for _, file := range files {
m, err := load(file)
if err != nil {
return nil, err
}
dirs := map[string]string{}
for _, r := range m.Resources {
switch {
case r.ID != "" && r.Path != "":
dirs[r.ID] = r.Path
case r.ID != "" && r.Type == "directory":
// A directory the mesh places for the module (`place`): where it lies is the node-engine's, but it is
// the module's own, so a path under it belongs to this module and to no other's directory of the same
// id (issue 1484: two runs-as modules each keep a .ssh in their own state directory).
dirs[r.ID] = "/placed/" + m.Module + "/" + r.ID
}
}
for _, r := range m.Resources {
p := norm(r.Path, dirs)
if p != "" {
paths = append(paths, declared{m.Module, r.ID, p})
}
if r.Type != "directory" && r.Type != "archive" {
continue
}
if r.Type == "directory" && p != "" {
containers = append(containers, container{m.Module, p})
}
add := func(rule int, why string) { out = append(out, Finding{rule, m.Module, r.ID, why}) }
switch {
case r.Unknown == "":
add(1, "a "+r.Type+" without `unknown` (move-out, module or program)")
case !values[r.Unknown]:
add(1, fmt.Sprintf("`unknown` is %q, not move-out, module or program", r.Unknown))
case r.Unknown == "move-out" && !underHome(p):
add(2, fmt.Sprintf("`move-out` on %q, which is not under ${machine:account-home} or ${machine:agent-home}", r.Path))
}
seen := map[string]bool{}
for _, k := range r.Knows {
if why := badKnows(k); why != "" {
add(3, fmt.Sprintf("`knows` entry %q %s", k, why))
continue
}
c := strings.TrimSuffix(k, "/")
if seen[c] {
add(3, fmt.Sprintf("`knows` entry %q is repeated", k))
}
seen[c] = true
}
if r.HasKnows && len(r.Knows) == 0 {
add(3, "`knows` is empty")
}
}
for _, f := range m.Facts {
if p := norm(Home+"/"+f, nil); p != "" {
paths = append(paths, declared{m.Module, "fact " + f, p})
}
}
}
for _, d := range paths {
for _, c := range containers {
if c.module != d.module && (d.path == c.path || strings.HasPrefix(d.path, c.path+"/")) {
out = append(out, Finding{4, d.module, d.id, fmt.Sprintf("declares %q, inside a directory of %s (%q)", d.path, c.module, c.path)})
}
}
}
return out, nil
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
found, err := Check(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
for _, f := range found {
fmt.Printf("%s: %s: %s (rule %d, novox/hq ADR 0422 §8, issue 1024)\n", f.Module, f.ID, f.Why, f.Rule)
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("owned paths: every directory and archive says how it treats unknown files; no module declares inside another's directory")
}
+147
View File
@@ -0,0 +1,147 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
func manifest(t *testing.T, root, module, body string) {
t.Helper()
dir := filepath.Join(root, "modules", module)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
func check(t *testing.T, root string) []Finding {
t.Helper()
found, err := Check(root)
if err != nil {
t.Fatal(err)
}
return found
}
// only asserts exactly one finding, of the rule, naming the module and the resource (issue 1024).
func only(t *testing.T, found []Finding, rule int, module, id string) {
t.Helper()
if len(found) != 1 || found[0].Rule != rule || found[0].Module != module || found[0].ID != id || found[0].Why == "" {
t.Fatalf("found %+v; want one finding of rule %d for %s/%s with a reason", found, rule, module, id)
}
}
func dir(id, path, rest string) string {
return `{"id":"` + id + `","type":"directory","path":"` + path + `"` + rest + `}`
}
func mod(name string, resources ...string) string {
return `{"module":"` + name + `","resources":[` + strings.Join(resources, ",") + `]}`
}
func TestMissingOrUnknownValueIsRefused(t *testing.T) {
root := t.TempDir()
manifest(t, root, "a", mod("a", dir("d", "/var/lib/a", "")))
only(t, check(t, root), 1, "a", "d")
root = t.TempDir()
manifest(t, root, "a", mod("a", `{"id":"z","type":"archive","path":"/opt/a","unknown":"delete"}`))
only(t, check(t, root), 1, "a", "z")
}
func TestMoveOutOutsideAHomeIsRefused(t *testing.T) {
root := t.TempDir()
manifest(t, root, "a", mod("a", dir("d", "/etc/a.d", `,"unknown":"move-out"`)))
only(t, check(t, root), 2, "a", "d")
}
func TestMoveOutUnderAHomeIsAllowed(t *testing.T) {
root := t.TempDir()
manifest(t, root, "a", mod("a",
dir("d", "${machine:account-home}/.config/a", `,"unknown":"move-out"`),
dir("e", "${dir:d}/sub", `,"unknown":"move-out"`),
dir("f", "${machine:agent-home}/x", `,"unknown":"move-out"`),
dir("g", "/var/lib/a", `,"unknown":"program"`),
dir("h", "${machine:account-home}/.ssh", `,"unknown":"module"`)))
if found := check(t, root); len(found) != 0 {
t.Fatalf("refused %+v", found)
}
}
func TestBadKnowsIsRefused(t *testing.T) {
for _, knows := range []string{`[""]`, `["/abs"]`, `["a/../b"]`, `["./a"]`, `["a//b"]`, `["a","a"]`, `["a","a/"]`} {
root := t.TempDir()
manifest(t, root, "a", mod("a", dir("d", "${machine:account-home}/.config/a", `,"unknown":"move-out","knows":`+knows)))
only(t, check(t, root), 3, "a", "d")
}
root := t.TempDir()
manifest(t, root, "a", mod("a", dir("d", "${machine:account-home}/.config/a", `,"unknown":"move-out","knows":["forge.credentials","kept/"]`)))
if found := check(t, root); len(found) != 0 {
t.Fatalf("refused %+v", found)
}
}
func TestAnotherModulesPathInsideADirectoryIsRefused(t *testing.T) {
root := t.TempDir()
manifest(t, root, "owner", mod("owner", dir("ssh", "${machine:account-home}/.ssh", `,"unknown":"module"`)))
manifest(t, root, "intruder", mod("intruder", `{"id":"f","type":"file","path":"${machine:account-home}/.ssh/config.d/20"}`))
manifest(t, root, "viahome", mod("viahome", `{"id":"g","type":"file","path":"${machine:agent-home}/.ssh/x"}`))
manifest(t, root, "facty", `{"module":"facty","facts":{"hosts":{"path":".ssh/config.d/00","home":true}}}`)
manifest(t, root, "outside", mod("outside", `{"id":"o","type":"file","path":"${machine:account-home}/.sshrc"}`))
got := map[string]int{}
for _, f := range check(t, root) {
if f.Rule != 4 || f.Why == "" {
t.Fatalf("finding %+v", f)
}
got[f.Module]++
}
if len(got) != 3 || got["intruder"] != 1 || got["viahome"] != 1 || got["facty"] != 1 {
t.Fatalf("refused %v; want intruder, viahome, facty", got)
}
}
func TestDirReferencesAreResolvedPerModule(t *testing.T) {
root := t.TempDir()
manifest(t, root, "owner", mod("owner", dir("base", "${machine:account-home}/.config/o", `,"unknown":"move-out"`)))
manifest(t, root, "other", mod("other",
dir("base", "${machine:account-home}/.config/p", `,"unknown":"move-out"`),
`{"id":"f","type":"file","path":"${dir:base}/x"}`))
if found := check(t, root); len(found) != 0 {
t.Fatalf("refused %+v", found)
}
manifest(t, root, "third", mod("third",
dir("d", "${machine:account-home}/.config/t", `,"unknown":"move-out"`),
`{"id":"f","type":"file","path":"${dir:d}/../o/x"}`))
only(t, check(t, root), 4, "third", "f")
}
func TestAnArchiveIsNotAContainerAndSameModuleIsFine(t *testing.T) {
root := t.TempDir()
manifest(t, root, "a", mod("a",
`{"id":"arc","type":"archive","path":"/opt/a","unknown":"program"}`,
dir("d", "/opt/a2", `,"unknown":"program"`),
`{"id":"f","type":"file","path":"/opt/a2/x"}`))
manifest(t, root, "b", mod("b", `{"id":"f","type":"file","path":"/opt/a/x"}`))
if found := check(t, root); len(found) != 0 {
t.Fatalf("refused %+v", found)
}
}
// Issue 1484: a directory the mesh places for a module (no path, `place`) is that module's own: another module's
// directory of the same id, and a path under it, are not inside it.
func TestPlacedDirectoriesOfTwoModulesAreTheirOwn(t *testing.T) {
root := t.TempDir()
for _, name := range []string{"first", "second"} {
manifest(t, root, name, mod(name,
`{"id":"state","type":"directory","place":".","unknown":"program"}`,
dir("ssh", "${dir:state}/.ssh", `,"unknown":"program"`),
`{"id":"conf","type":"file","path":"${dir:ssh}/config"}`))
}
if found := check(t, root); len(found) != 0 {
t.Fatalf("refused %+v", found)
}
}
+3
View File
@@ -0,0 +1,3 @@
# The modules the check covers (novox/hq ADR 0507 §9, issue 1838), one a line; issue 1838 widens it.
pacman
kernel
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/particulars
go 1.22
+159
View File
@@ -0,0 +1,159 @@
// Command particulars keeps this mesh's node names out of the modules it covers (novox/hq ADR 0507 §9,
// issue 1838): a module is for any mesh, so no manifest, file, code string, identifier, comment, test name or
// fixture of a covered module names a node of this one.
//
// The names are in the file `particulars` at the catalogue's root, one a line, `#` comments — the one place
// they may be written. The modules covered are in `covered` at checks/particulars/covered, one a line. A name is matched
// whole and in any case; a hyphen, an underscore or a CamelCase boundary parts one, so a longer word holding a name is another word.
// The forge is not a node name: its host (git.novox.be) and the organisation path (novox/<repo>) are blanked
// before a line is read.
//
// go run . <catalogue root>
//
// Exits 1 naming file:line and the word, 2 when a list cannot be read.
package main
import (
"bufio"
"bytes"
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// forge is the forge's host and organisation path, blanked before a line is read; a path segment novox/ elsewhere
// (testdata/novox/x, /srv/novox/data) is not blanked.
var forge = regexp.MustCompile(`(?i)git\.novox\.be(?:/novox/[\w.-]+)?|(?:^|[^/\w.-])novox/[\w.-]+`)
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
os.Exit(check(root))
}
// check runs the check over a catalogue and returns the exit code: 0 clean, 1 a finding, 2 a list unreadable.
func check(root string) int {
names, err := readList(filepath.Join(root, "particulars"))
if err == nil && len(names) == 0 {
err = fmt.Errorf("particulars lists no name — the check would pass on anything")
}
var covered []string
if err == nil {
covered, err = readList(filepath.Join(root, "checks/particulars/covered"))
if err == nil && len(covered) == 0 {
err = fmt.Errorf("covered lists no module — the check would pass on anything")
}
}
if err != nil {
fmt.Fprintln(os.Stderr, "particulars:", err)
return 2
}
findings, err := run(root, names, covered)
if err != nil {
fmt.Fprintln(os.Stderr, "particulars:", err)
return 2
}
for _, f := range findings {
fmt.Println(f)
}
if len(findings) > 0 {
fmt.Printf("particulars: %d node name(s) in a module for any mesh (novox/hq ADR 0507 §9) — say what it shows, not where it was seen\n", len(findings))
return 1
}
fmt.Printf("particulars: none of %d node names in the %d covered modules\n", len(names), len(covered))
return 0
}
// readList reads a list: one entry a line; blank lines and lines starting with # are not entries.
func readList(path string) ([]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var out []string
sc := bufio.NewScanner(f)
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
out = append(out, line)
}
return out, sc.Err()
}
// run reads every text file of each covered module and returns the findings, sorted.
func run(root string, names, covered []string) ([]string, error) {
var out []string
for _, m := range covered {
dir := filepath.Join(root, "modules", m)
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
switch d.Name() {
case "node_modules", "dist", "vendor", ".git":
return filepath.SkipDir
}
return nil
}
rel, _ := filepath.Rel(root, path)
// A path is read too: a testdata directory named after a node is a finding.
for _, n := range hits(filepath.ToSlash(rel), names) {
out = append(out, fmt.Sprintf("%s:0: node name %q in the path", filepath.ToSlash(rel), n))
}
if d.Name() == "go.sum" {
return nil
}
b, err := os.ReadFile(path)
if err != nil {
return err
}
if bytes.IndexByte(b, 0) >= 0 {
return nil
}
out = append(out, scan(filepath.ToSlash(rel), string(b), names)...)
return nil
})
if err != nil {
return nil, err
}
}
sort.Strings(out)
return out, nil
}
// scan returns a finding for each node name in src, whole-word and in any case.
func scan(file, src string, names []string) []string {
var out []string
for i, line := range strings.Split(src, "\n") {
for _, n := range hits(line, names) {
out = append(out, fmt.Sprintf("%s:%d: node name %q — %s", file, i+1, n, strings.Join(strings.Fields(line), " ")))
}
}
return out
}
// hits returns the node names one line holds: as a word (a hyphen or underscore parts it), as a part of a
// CamelCase identifier (TestAce, novoxTree), the forge's host and organisation path blanked first.
func hits(line string, names []string) []string {
line = forge.ReplaceAllStringFunc(line, func(s string) string { return strings.Repeat(" ", len(s)) })
var out []string
for _, n := range names {
q := regexp.QuoteMeta(n)
camel := regexp.QuoteMeta(strings.ToUpper(n[:1]) + n[1:])
rx := regexp.MustCompile(`(?i:(?:^|[^A-Za-z0-9])` + q + `(?:$|[^A-Za-z0-9]))|[a-z0-9]` + camel + `(?:$|[^a-z])|(?:^|[^A-Za-z0-9])` + q + `[A-Z0-9]`)
if rx.MatchString(line) {
out = append(out, strings.ToLower(n))
}
}
return out
}
+116
View File
@@ -0,0 +1,116 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
var names = []string{"novox", "ace", "shanks", "g14"}
func TestANodeNameIsRefusedWhereItIsWritten(t *testing.T) {
cases := map[string]struct{ file, src string }{
"manifest string": {"modules/kernel/module.json", "{\n \"description\": \"as on Shanks\"\n}\n"},
"file line": {"modules/kernel/files/boot.sh", "#!/bin/sh\necho run on g14\n"},
"go identifier": {"modules/kernel/cmd/x/a.go", "package main\n\nfunc novox() {}\n"},
"test name": {"modules/kernel/cmd/x/a_test.go", "package main\n\nfunc TestAce(t *testing.T) {}\n\nfunc TestOnAce_x(t *testing.T) {}\n"},
}
for name, c := range cases {
if got := scan(c.file, c.src, names); len(got) == 0 {
t.Errorf("%s: %q not refused", name, c.src)
}
}
}
func TestTheFindingNamesTheLineAndTheWord(t *testing.T) {
got := scan("f.sh", "a\nb\nrun on ACE now\n", names)
if len(got) != 1 || !strings.HasPrefix(got[0], "f.sh:3:") || !strings.Contains(got[0], "ace") {
t.Errorf("got %v", got)
}
}
func TestTheForgeIsNotANodeName(t *testing.T) {
for _, s := range []string{
"see novox/hq issue 5",
"module git.novox.be/novox/mesh-sdk/go v1.2.3",
"https://git.novox.be/novox/mesh-catalog",
"git.novox.be",
"space trace surface grace g14x",
} {
if got := scan("f", s, names); len(got) != 0 {
t.Errorf("%q refused: %v", s, got)
}
}
}
func TestANodeNameInAPathOrJoinedIsRefused(t *testing.T) {
for _, s := range []string{
"modules/kernel/testdata/novox/meminfo",
"/srv/novox/data",
"testdata/shanks-meminfo.txt",
"on-shanks",
"shanks-desktop",
"mesh-novox",
"func novoxTree() {}",
"aceFiles := 1",
"g14Meminfo := 2",
"novox_tree := 3",
} {
if got := scan("f", s, names); len(got) == 0 {
t.Errorf("%q not refused", s)
}
}
}
func TestANodeNamedPathIsRefusedThroughRun(t *testing.T) {
root := t.TempDir()
p := filepath.Join(root, "modules/kernel/testdata/ace/x")
os.MkdirAll(filepath.Dir(p), 0o755)
os.WriteFile(p, []byte("clean\n"), 0o644)
got, err := run(root, names, []string{"kernel"})
if err != nil || len(got) != 1 || !strings.HasPrefix(got[0], "modules/kernel/testdata/ace/x:0: ") {
t.Errorf("got %v, %v", got, err)
}
}
func TestCheckReturnsOneOnAFindingAndTwoOnAnUnreadableList(t *testing.T) {
root := t.TempDir()
write := func(p, s string) {
p = filepath.Join(root, p)
os.MkdirAll(filepath.Dir(p), 0o755)
os.WriteFile(p, []byte(s), 0o644)
}
write("particulars", "ace\n")
write("checks/particulars/covered", "kernel\n")
write("modules/kernel/a", "clean\n")
if got := check(root); got != 0 {
t.Errorf("clean = %d, want 0", got)
}
write("modules/kernel/a", "on ace\n")
if got := check(root); got != 1 {
t.Errorf("finding = %d, want 1", got)
}
os.Remove(filepath.Join(root, "particulars"))
if got := check(root); got != 2 {
t.Errorf("no list = %d, want 2", got)
}
}
func TestOnlyCoveredModulesAreRead(t *testing.T) {
root := t.TempDir()
write := func(p, s string) {
p = filepath.Join(root, p)
os.MkdirAll(filepath.Dir(p), 0o755)
os.WriteFile(p, []byte(s), 0o644)
}
write("modules/kernel/files/a", "on novox\n")
write("modules/other/files/a", "on novox\n")
got, err := run(root, names, []string{"kernel"})
if err != nil {
t.Fatal(err)
}
if len(got) != 1 || !strings.Contains(got[0], "modules/kernel/files/a:1") {
t.Errorf("got %v", got)
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/predefined
go 1.22
+153
View File
@@ -0,0 +1,153 @@
// Command predefined refuses a change to predefined.json that removes a key a module still names (novox/hq ADR
// 0348 clause 7, design 57 package K2, issue 661).
//
// Mesh config and node config hold values only for the keys on the catalogue's list of predefined keys, at the
// root of the repository; a module's config value may fall back to one, `"predefined": "<key>"`. A key removed
// while a manifest still names it would leave that module falling back to nothing: the controller refuses its
// build then, at the next merge that touches it. So the removal is refused here, in the pull request that makes
// it, naming each module that names the key.
//
// **What it cannot know.** Whether a key still holds a value at the mesh or a node is mesh-config's to say (design
// 57 package K3): it holds the values, and the catalogue's check reads no store. Until mesh-config refuses that
// itself, removing a key nobody names is let through here, and said.
//
// The list it compares with is the trunk's at the merge base, given with -base; without one, nothing was removed.
// The controller the mesh runs reads the list itself (type, validation, every manifest against it) in the
// module check; this check reads only its keys.
//
// go run . [-base <the trunk's predefined.json>] <catalogue root>
//
// Exits 1 on a finding.
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
)
// File is the list's file at the root of the catalogue.
const File = "predefined.json"
// Keys is the keys of a list, read as a JSON object; an absent file has none.
func Keys(path string) (map[string]bool, error) {
raw, err := os.ReadFile(path)
if errors.Is(err, fs.ErrNotExist) {
return map[string]bool{}, nil
}
if err != nil {
return nil, err
}
var keys map[string]json.RawMessage
if err := json.Unmarshal(raw, &keys); err != nil {
return nil, fmt.Errorf("%s is not a JSON object of keys: %w", path, err)
}
out := map[string]bool{}
for k := range keys {
out[k] = true
}
return out, nil
}
// Named is, per predefined key, the modules whose manifests name it: under `config`, or its alias `settings`.
func Named(root string) (map[string][]string, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
named := map[string][]string{}
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Config map[string]json.RawMessage `json:"config"`
Settings map[string]json.RawMessage `json:"settings"`
}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
for _, declared := range []map[string]json.RawMessage{m.Config, m.Settings} {
for _, d := range declared {
var value struct {
Predefined string `json:"predefined"`
}
if json.Unmarshal(d, &value) == nil && value.Predefined != "" {
named[value.Predefined] = append(named[value.Predefined], m.Module)
}
}
}
}
return named, nil
}
// Removed is each key the base list holds and the tree's does not, with the modules still naming it; a removed
// key nobody names is answered with none.
func Removed(base, tree map[string]bool, named map[string][]string) map[string][]string {
out := map[string][]string{}
for k := range base {
if !tree[k] {
out[k] = named[k]
}
}
return out
}
func main() {
basePath := flag.String("base", "", "the trunk's predefined.json at the merge base")
flag.Parse()
if flag.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: predefined [-base <the trunk's predefined.json>] <catalogue root>")
os.Exit(2)
}
root := flag.Arg(0)
tree, err := Keys(filepath.Join(root, File))
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if *basePath == "" {
fmt.Printf("%s: %d key(s); no trunk list given, so none was removed\n", File, len(tree))
return
}
base, err := Keys(*basePath)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
named, err := Named(root)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
removed := Removed(base, tree, named)
keys := make([]string, 0, len(removed))
for k := range removed {
keys = append(keys, k)
}
sort.Strings(keys)
failed := false
for _, k := range keys {
if modules := removed[k]; len(modules) > 0 {
failed = true
fmt.Printf("%s: the key %q is removed and still named by %s: move each off it first (novox/hq ADR 0348 "+
"clause 7)\n", File, k, strings.Join(modules, ", "))
} else {
fmt.Printf("%s: the key %q is removed and no module names it; whether the mesh or a node still holds a "+
"value for it is mesh-config's to refuse, not this check's\n", File, k)
}
}
if failed {
os.Exit(1)
}
fmt.Printf("%s: %d key(s), none removed that a module names\n", File, len(tree))
}
+100
View File
@@ -0,0 +1,100 @@
package main
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// A catalogue whose dunst falls back to font-size, and the trunk's list holding font-size and
// agent-managed-settings.
func aCatalogue(t *testing.T, list string) (root, base string) {
t.Helper()
root = t.TempDir()
write := func(path, text string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(text), 0o644); err != nil {
t.Fatal(err)
}
}
write(filepath.Join(root, "modules", "dunst", "module.json"), `{"module": "dunst", "config": {"font-size":
{"type": "number", "description": "points", "default": 10, "predefined": "font-size"}}}`)
write(filepath.Join(root, "modules", "claude-code", "module.json"), `{"module": "claude-code", "settings": {
"managed_settings": {"kind": "preference", "default": "{}", "why": "none"}}}`)
write(filepath.Join(root, "modules", "plain", "module.json"), `{"module": "plain"}`)
if list != "" {
write(filepath.Join(root, File), list)
}
base = filepath.Join(t.TempDir(), "base.json")
write(base, `{"font-size": {"type": "number"}, "agent-managed-settings": {"type": "object"}}`)
return root, base
}
func run(t *testing.T, args ...string) (string, error) {
t.Helper()
out, err := exec.Command("go", append([]string{"run", "."}, args...)...).CombinedOutput()
return string(out), err
}
func TestARemovalOfAKeyAModuleStillNamesIsRefused(t *testing.T) {
// The planted removal: font-size goes while dunst still falls back to it.
root, base := aCatalogue(t, `{"agent-managed-settings": {"type": "object"}}`)
out, err := run(t, "-base", base, root)
if err == nil || !strings.Contains(out, `the key "font-size" is removed and still named by dunst`) {
t.Fatalf("a removal of a named key passed:\n%s", out)
}
}
func TestARemovalNobodyNamesPassesAndSaysWhatItCannotKnow(t *testing.T) {
root, base := aCatalogue(t, `{"font-size": {"type": "number"}}`)
out, err := run(t, "-base", base, root)
if err != nil || !strings.Contains(out, `"agent-managed-settings" is removed and no module names it`) ||
!strings.Contains(out, "mesh-config's to refuse") {
t.Fatalf("a removal nobody names: %v\n%s", err, out)
}
}
func TestAListUnchangedOrGrownPasses(t *testing.T) {
for _, list := range []string{
`{"font-size": {"type": "number"}, "agent-managed-settings": {"type": "object"}}`,
`{"font-size": {"type": "number"}, "agent-managed-settings": {"type": "object"}, "accent": {"type": "string"}}`,
} {
root, base := aCatalogue(t, list)
if out, err := run(t, "-base", base, root); err != nil || !strings.Contains(out, "none removed") {
t.Fatalf("%s: %v\n%s", list, err, out)
}
}
// No trunk list given: nothing was removed.
root, _ := aCatalogue(t, `{}`)
if out, err := run(t, root); err != nil || !strings.Contains(out, "none was removed") {
t.Fatalf("without a base: %v\n%s", err, out)
}
}
func TestTheFileRemovedWholeIsARemovalOfEveryKey(t *testing.T) {
root, base := aCatalogue(t, "")
out, err := run(t, "-base", base, root)
if err == nil || !strings.Contains(out, `"font-size" is removed and still named by dunst`) {
t.Fatalf("the list removed whole passed:\n%s", out)
}
}
func TestNamedReadsBothForms(t *testing.T) {
root, _ := aCatalogue(t, "")
if err := os.WriteFile(filepath.Join(root, "modules", "plain", "module.json"), []byte(`{"module": "plain",
"settings": {"size": {"predefined": "font-size"}}}`), 0o644); err != nil {
t.Fatal(err)
}
named, err := Named(root)
if err != nil {
t.Fatal(err)
}
if got := strings.Join(named["font-size"], ","); got != "dunst,plain" {
t.Fatalf("font-size named by %s", got)
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/secrets
go 1.22
+4
View File
@@ -0,0 +1,4 @@
# Secrets nested below a key a mergeable file takes for its other values, which the controller cannot
# protect alone yet (it protects top-level keys): "<module> <resource> <dotted key>". This list only shrinks:
# a line that no longer names a finding is refused, and so is one the trunk's copy does not hold (hq issue 342).
searxng settings server.secret_key
+254
View File
@@ -0,0 +1,254 @@
// Command secrets refuses, in every module's manifest, a mergeable file that a setting could fill with a
// password, a token or an API key in place of the vault's (novox/hq issue 342).
//
// A password is not a setting: it comes from the key vault, and it can be rotated there. A file that says
// `"merge": "json"` takes the keys of the mesh-wide and the node's layers over its own content, so a key the
// content fills with `${secret:<name>}` is one a layer could replace, and the vault's value would lose to it
// until somebody removed it by hand. The controller refuses a setting of a key the file keeps as `protected`,
// naming the layer and the key, never the value. This check holds every such file to that: each top-level key
// whose value holds `${secret:…}`, anywhere below it, is either listed in the file's `protected`, or left out
// of the file's `takes-settings` (novox/hq ADR 0326), so that no layer reaches it. A file whose text is a template
// in files/ is judged by the template's text.
//
// A secret nested below a key the file takes for its other values (`server.secret_key` below `server`) cannot
// be protected alone today: the controller protects top-level keys. Such a file is named in the file `known`
// beside this check, one line per key, until the controller refuses a nested key. The list only shrinks: a
// line there that no longer names a finding is refused, and so is a line the trunk's copy (the merge base's,
// given with -base) does not hold. While the trunk holds no copy, the lines in seed are the trunk's.
//
// go run . [-base <the trunk's known>] <catalogue root>
//
// Exits 1 on a finding.
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
// Finding is one key of one mergeable file a layer could fill in place of the vault.
type Finding struct {
Module, ID, Key string
}
func (f Finding) String() string { return f.Module + " " + f.ID + " " + f.Key }
// Check reads every manifest under root/modules and answers each secret-bearing key a layer could replace.
func Check(root string) ([]Finding, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
var out []Finding
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Resources []map[string]any `json:"resources"`
}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
for _, r := range m.Resources {
if how, _ := r["merge"].(string); how == "" {
continue
}
id, _ := r["id"].(string)
content, _ := r["content"].(string)
// A file whose text is a template in its module's files/ (novox/hq ADR 0349 §2) is judged by that text,
// exactly as it would be inline.
if name, _ := r["template"].(string); name != "" {
text, err := os.ReadFile(filepath.Join(filepath.Dir(path), filepath.FromSlash(name)))
if err != nil {
return nil, fmt.Errorf("%s: %s names the template %s: %w", m.Module, id, name, err)
}
content = string(text)
}
var doc map[string]any
if strings.TrimSpace(content) == "" {
continue
}
if err := json.Unmarshal([]byte(content), &doc); err != nil {
return nil, fmt.Errorf("%s: %s merges as JSON and its content is not JSON: %w", m.Module, id, err)
}
protected := set(r["protected"])
takes, names := r["takes-settings"]
taken := set(takes)
for _, key := range sortedKeys(doc) {
for _, at := range secretsBelow(key, doc[key]) {
switch {
case protected[key]:
case names && !taken[key]:
default:
out = append(out, Finding{m.Module, id, at})
}
}
}
}
}
return out, nil
}
// secretsBelow answers the dotted path of every string at or below value that names a vault secret.
func secretsBelow(at string, value any) []string {
switch v := value.(type) {
case string:
if strings.Contains(v, "${secret:") {
return []string{at}
}
case map[string]any:
var out []string
for _, k := range sortedKeys(v) {
out = append(out, secretsBelow(at+"."+k, v[k])...)
}
return out
case []any:
var out []string
for i, e := range v {
out = append(out, secretsBelow(fmt.Sprintf("%s[%d]", at, i), e)...)
}
return out
}
return nil
}
func set(v any) map[string]bool {
out := map[string]bool{}
list, _ := v.([]any)
for _, e := range list {
if s, ok := e.(string); ok {
out[s] = true
}
}
return out
}
func sortedKeys(m map[string]any) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
// Known reads the lines of the known list: "<module> <resource> <key>", # comments and blank lines skipped.
func Known(path string) (map[string]bool, error) {
f, err := os.Open(path)
if os.IsNotExist(err) {
return map[string]bool{}, nil
}
if err != nil {
return nil, err
}
defer f.Close()
out := map[string]bool{}
s := bufio.NewScanner(f)
for s.Scan() {
line := strings.TrimSpace(s.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
out[strings.Join(strings.Fields(line), " ")] = true
}
return out, s.Err()
}
// seed is the known list as it was first written, standing for the trunk's copy until the trunk holds one.
var seed = map[string]bool{
"searxng settings server.secret_key": true,
}
// Added answers the lines of known that base does not hold: a line may leave the list, never join it.
func Added(known, base map[string]bool) []string {
var out []string
for line := range known {
if !base[line] {
out = append(out, line)
}
}
sort.Strings(out)
return out
}
// Judge splits the findings into those refused and the known lines that no longer name one.
func Judge(found []Finding, known map[string]bool) (refused []Finding, stale []string) {
seen := map[string]bool{}
for _, f := range found {
seen[f.String()] = true
if !known[f.String()] {
refused = append(refused, f)
}
}
for line := range known {
if !seen[line] {
stale = append(stale, line)
}
}
sort.Strings(stale)
return refused, stale
}
// remedy says how a finding is put right.
func remedy(key string) string {
top, _, nested := strings.Cut(key, ".")
if !nested {
return fmt.Sprintf("list %q in the file's \"protected\", so the controller refuses a setting of it", top)
}
return fmt.Sprintf("the controller protects a top-level key only; list %q in the file's \"protected\" if "+
"nothing else below it is the operator's, or leave it out of the file's \"takes-settings\"", top)
}
func main() {
basePath := flag.String("base", "", "the trunk's copy of known (the merge base's); seed when absent")
flag.Parse()
root := "."
if flag.NArg() > 0 {
root = flag.Arg(0)
}
found, err := Check(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
known, err := Known("known")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
base := seed
if *basePath != "" {
if base, err = Known(*basePath); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
added := Added(known, base)
for _, line := range added {
fmt.Printf("checks/secrets/known gains %q, which the trunk's copy does not hold: the list only shrinks — "+
"protect the key instead (novox/hq issue 342)\n", line)
}
refused, stale := Judge(found, known)
for _, f := range refused {
fmt.Printf("%s: %s fills %s with a vault secret, and a setting could replace it (novox/hq issue 342): %s\n",
f.Module, f.ID, f.Key, remedy(f.Key))
}
for _, line := range stale {
fmt.Printf("checks/secrets/known names %q, which no longer holds a secret a setting could replace: "+
"remove the line\n", line)
}
if len(refused) > 0 || len(stale) > 0 || len(added) > 0 {
os.Exit(1)
}
fmt.Printf("secrets: no setting can replace a vault secret in a mergeable file (%d known, nested)\n", len(known))
}
+154
View File
@@ -0,0 +1,154 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func manifest(t *testing.T, root, module, resources string) {
t.Helper()
dir := filepath.Join(root, "modules", module)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
body := `{"module":"` + module + `","resources":[` + resources + `]}`
if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
func TestASecretASettingCouldReplaceIsRefused(t *testing.T) {
root := t.TempDir()
// Refused: the key holds the vault's secret and nothing keeps a layer off it.
manifest(t, root, "a", `{"id":"cfg","type":"file","merge":"json","content":"{\"password\": \"${secret:admin}\"}"}`)
// Refused: nested below a key the file takes.
manifest(t, root, "b", `{"id":"cfg","type":"file","merge":"json","takes-settings":["server"],`+
`"content":"{\"server\": {\"secret_key\": \"${secret:s}\", \"port\": 1}}"}`)
// Kept: protected.
manifest(t, root, "c", `{"id":"cfg","type":"file","merge":"json","protected":["password"],`+
`"content":"{\"password\": \"${secret:admin}\", \"user\": \"admin\"}"}`)
// Kept: the file names its keys, and not this one.
manifest(t, root, "d", `{"id":"cfg","type":"file","merge":"json","takes-settings":["ui"],`+
`"content":"{\"token\": \"${secret:t}\", \"ui\": {}}"}`)
// Kept: a file that does not merge takes no setting.
manifest(t, root, "e", `{"id":"cfg","type":"file","content":"{\"token\": \"${secret:t}\"}"}`)
// Refused: protecting another key does not protect this one; a list element counts too.
manifest(t, root, "f", `{"id":"cfg","type":"file","merge":"json","protected":["user"],`+
`"content":"{\"users\": [{\"password\": \"${secret:p}\"}]}"}`)
found, err := Check(root)
if err != nil {
t.Fatal(err)
}
want := []string{"a cfg password", "b cfg server.secret_key", "f cfg users[0].password"}
if len(found) != len(want) {
t.Fatalf("found %v, want %v", found, want)
}
for i, f := range found {
if f.String() != want[i] {
t.Fatalf("found %v, want %v", found, want)
}
}
}
// A mergeable file whose text is a template in files/ (novox/hq ADR 0349 §2) is judged by the template's text,
// exactly as the same text inline; a template the module's directory lacks is an error, never a silent pass.
func TestAMergeableFilesTemplateIsJudgedAsItsText(t *testing.T) {
root := t.TempDir()
manifest(t, root, "a", `{"id":"cfg","type":"file","merge":"json","template":"files/cfg.json"}`)
manifest(t, root, "c", `{"id":"cfg","type":"file","merge":"json","protected":["password"],"template":"files/cfg.json"}`)
for _, m := range []string{"a", "c"} {
dir := filepath.Join(root, "modules", m, "files")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "cfg.json"), []byte(`{"password": "${secret:admin}"}`), 0o644); err != nil {
t.Fatal(err)
}
}
found, err := Check(root)
if err != nil {
t.Fatal(err)
}
if len(found) != 1 || found[0].String() != "a cfg password" {
t.Fatalf("found %v, want [a cfg password]", found)
}
manifest(t, root, "g", `{"id":"cfg","type":"file","merge":"json","template":"files/gone.json"}`)
if _, err := Check(root); err == nil {
t.Fatal("a template the module's directory lacks passed the check")
}
}
func TestTheKnownListOnlyShrinks(t *testing.T) {
found := []Finding{{"b", "cfg", "server.secret_key"}, {"a", "cfg", "password"}}
refused, stale := Judge(found, map[string]bool{"b cfg server.secret_key": true, "gone cfg token": true})
if len(refused) != 1 || refused[0].String() != "a cfg password" {
t.Fatalf("refused %v", refused)
}
if len(stale) != 1 || stale[0] != "gone cfg token" {
t.Fatalf("stale %v", stale)
}
}
func TestTheCatalogueHoldsNoSecretASettingCouldReplace(t *testing.T) {
found, err := Check("../..")
if err != nil {
t.Fatal(err)
}
known, err := Known("known")
if err != nil {
t.Fatal(err)
}
refused, stale := Judge(found, known)
if len(refused) > 0 || len(stale) > 0 {
t.Fatalf("refused %v, stale %v", refused, stale)
}
}
func TestALineTheTrunkDoesNotHoldIsRefused(t *testing.T) {
trunk := map[string]bool{"b cfg server.secret_key": true}
// A key unprotected on the branch, and its line added to the list: refused, though it matches a finding.
branch := map[string]bool{"b cfg server.secret_key": true, "a cfg password": true}
if added := Added(branch, trunk); len(added) != 1 || added[0] != "a cfg password" {
t.Fatalf("added %v", added)
}
// A line removed is no addition.
if added := Added(map[string]bool{}, trunk); len(added) != 0 {
t.Fatalf("added %v", added)
}
// Until the trunk holds a copy, the seed stands for it, and the list as committed is within it.
known, err := Known("known")
if err != nil {
t.Fatal(err)
}
if added := Added(known, seed); len(added) != 0 {
t.Fatalf("known holds lines the seed does not: %v", added)
}
}
func TestTheCheckRefusesALineAddedAgainstTheTrunk(t *testing.T) {
root := t.TempDir()
manifest(t, root, "a", `{"id":"cfg","type":"file","merge":"json","content":"{\"password\": \"${secret:admin}\"}"}`)
dir := t.TempDir()
base := filepath.Join(dir, "base")
if err := os.WriteFile(base, []byte("# the trunk's\n"), 0o644); err != nil {
t.Fatal(err)
}
branch := map[string]bool{"a cfg password": true}
found, err := Check(root)
if err != nil {
t.Fatal(err)
}
trunk, err := Known(base)
if err != nil {
t.Fatal(err)
}
refused, stale := Judge(found, branch)
if len(refused) != 0 || len(stale) != 0 {
t.Fatalf("the line hides the finding from Judge alone: %v %v", refused, stale)
}
if added := Added(branch, trunk); len(added) != 1 {
t.Fatalf("an added line passed: %v", added)
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/sshdir
go 1.22
+161
View File
@@ -0,0 +1,161 @@
// Command sshdir refuses any manifest but modules/ssh-client naming a path under ~/.ssh (novox/hq ADR 0415 §6,
// issue 1020): ssh-client owns ~/.ssh and sweeps what it does not know, so a path another module placed there
// would be moved out at the next sweep. A module that needs something there provides it to ssh-client instead.
//
// go run . <catalogue root>
//
// Every string of a manifest is read, wherever it stands, except prose and a file's text (Prose): a path, a
// place, a volume; a path inside a directory of the module's own (`${dir:<id>}`) is not ~/.ssh. Exits 1 on a finding.
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// SSHModule is the one module allowed to name a path under ~/.ssh.
const SSHModule = "ssh-client"
// underSSH is a path under ~/.ssh, or ~/.ssh itself, however the home is spelled: ${machine:account-home}/.ssh,
// ~/.ssh, .ssh/config, or a volume's host side (…/.ssh:/root/.ssh).
var underSSH = regexp.MustCompile(`(^|[/~])\.ssh($|[/:])`)
// Prose are the keys whose strings are words or a file's text, not a path: a template may say ~/.ssh in a comment.
var Prose = map[string]bool{"description": true, "why": true, "does": true, "template": true, "content": true, "title": true}
// Finding is one path under ~/.ssh in a manifest other than ssh-client's.
type Finding struct {
Module string
Key string
Value string
}
// Check reads every manifest under root/modules and answers which paths it refuses.
func Check(root string) ([]Finding, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
var out []Finding
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m any
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
module := filepath.Base(filepath.Dir(path))
if obj, ok := m.(map[string]any); ok {
if name, ok := obj["module"].(string); ok && name != "" {
module = name
}
}
if module == SSHModule {
continue
}
walk(m, "", dirsOf(m), func(key, value string) {
out = append(out, Finding{Module: module, Key: key, Value: value})
})
}
return out, nil
}
// walk calls found for every string under v, outside Prose, that names a path under ~/.ssh.
func walk(v any, key string, dirs map[string]string, found func(key, value string)) {
switch x := v.(type) {
case map[string]any:
keys := make([]string, 0, len(x))
for k := range x {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if !Prose[k] {
walk(x[k], k, dirs, found)
}
}
case []any:
for _, e := range x {
walk(e, key, dirs, found)
}
case string:
// A path under a directory the mesh places for the module (${dir:<id>} of a directory with a place and no path,
// as a runs-as module's home) is that account's own .ssh, not the operator's ~/.ssh that ssh-client sweeps
// (issue 1484, ADR 0466 §4). Any other ${dir:<id>} is resolved to the path its resource declares and judged.
if resolved, placed := resolveDir(x, dirs); !placed && underSSH.MatchString(resolved) {
found(key, x)
}
}
}
var ofDir = regexp.MustCompile(`^\$\{dir:([a-z0-9][a-z0-9_-]*)\}`)
// placedDir marks a directory the mesh places for the module: it has no path of its own.
const placedDir = "\x00placed"
// dirsOf are the module's directories by id: the path each declares, or placedDir for one with a place and no path.
func dirsOf(m any) map[string]string {
dirs := map[string]string{}
obj, _ := m.(map[string]any)
list, _ := obj["resources"].([]any)
for _, r := range list {
res, _ := r.(map[string]any)
id, _ := res["id"].(string)
path, _ := res["path"].(string)
switch {
case id == "":
case path != "":
dirs[id] = path
case res["type"] == "directory":
dirs[id] = placedDir
}
}
return dirs
}
// resolveDir follows a leading ${dir:<id>} through the module's directories. It answers the path with the references
// resolved, and whether it ends under a placed directory.
func resolveDir(x string, dirs map[string]string) (string, bool) {
for i := 0; i < 8; i++ {
m := ofDir.FindStringSubmatch(x)
if m == nil {
return x, false
}
to, ok := dirs[m[1]]
if !ok {
return x, false
}
if to == placedDir {
return x, true
}
x = to + strings.TrimPrefix(x, m[0])
}
return x, false
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
found, err := Check(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
for _, f := range found {
fmt.Printf("%s names %q (%s) under ~/.ssh: only %s may, and the sweep moves anything else out (novox/hq ADR 0415 §6)\n", f.Module, f.Value, f.Key, SSHModule)
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("~/.ssh: named only by ssh-client")
}
+78
View File
@@ -0,0 +1,78 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func manifest(t *testing.T, root, module, body string) {
t.Helper()
dir := filepath.Join(root, "modules", module)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
func TestAnotherModuleNamingSSHIsRefused(t *testing.T) {
root := t.TempDir()
manifest(t, root, "ssh-client", `{"module":"ssh-client","resources":[{"path":"${machine:account-home}/.ssh/config"}]}`)
manifest(t, root, "forge-account", `{"module":"forge-account","resources":[{"path":"${machine:account-home}/.ssh/config.d/20-forge"}]}`)
manifest(t, root, "roster", `{"module":"roster","facts":{"hosts":{"path":".ssh/known_hosts"}}}`)
manifest(t, root, "box", `{"module":"box","containers":[{"volumes":["~/.ssh:/root/.ssh:ro"]}]}`)
manifest(t, root, "quiet", `{"module":"quiet","description":"reads ~/.ssh/config","resources":[{"path":"${machine:account-home}/.sshrc-not","template":"# see ~/.ssh/config"},{"path":"/etc/ssh/sshd_config"}]}`)
found, err := Check(root)
if err != nil {
t.Fatal(err)
}
got := map[string]bool{}
for _, f := range found {
got[f.Module] = true
}
if len(found) != 3 || !got["forge-account"] || !got["roster"] || !got["box"] {
t.Fatalf("refused %+v; want forge-account, roster and box", found)
}
}
// Issue 1484: a runs-as module's own account has a .ssh of its own, inside its home (a directory of the module's);
// that is not the operator's ~/.ssh, which only ssh-client names. A path outside any ${dir:} is still refused.
func TestAModulesOwnDirectoryMayHoldADotSSH(t *testing.T) {
root := t.TempDir()
manifest(t, root, "builder", `{"module":"builder","resources":[{"id":"state","type":"directory","place":"."},{"id":"ssh-dir","type":"directory","path":"${dir:state}/.ssh"},{"path":"${dir:ssh-dir}/config"}]}`)
manifest(t, root, "other", `{"module":"other","resources":[{"path":"${machine:account-home}/.ssh/config"}]}`)
found, err := Check(root)
if err != nil {
t.Fatal(err)
}
if len(found) != 1 || found[0].Module != "other" {
t.Fatalf("refused %+v; want only other", found)
}
}
// Issue 1484 review: only a directory the mesh places is the module's own home. A ${dir:} that resolves to the
// operator's home is judged by the path it resolves to.
func TestADirResolvingToTheOperatorsHomeIsStillRefused(t *testing.T) {
root := t.TempDir()
manifest(t, root, "escape", `{"module":"escape","resources":[{"id":"h","type":"directory","path":"${machine:account-home}","unknown":"module"},{"path":"${dir:h}/.ssh/config"}]}`)
manifest(t, root, "placed", `{"module":"placed","resources":[{"id":"state","type":"directory","place":".","unknown":"program"},{"id":"s","type":"directory","path":"${dir:state}/.ssh"},{"path":"${dir:s}/config"}]}`)
found, err := Check(root)
if err != nil {
t.Fatal(err)
}
if len(found) != 1 || found[0].Module != "escape" {
t.Fatalf("refused %+v; want only escape", found)
}
}
func TestTheCatalogueHasNone(t *testing.T) {
found, err := Check("../..")
if err != nil {
t.Fatal(err)
}
if len(found) != 0 {
t.Fatalf("the catalogue names paths under ~/.ssh outside ssh-client: %+v", found)
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/templates
go 1.22
+9
View File
@@ -0,0 +1,9 @@
# The modules that still keep a file's text inline in module.json (`content`), one per line: the catalogue's list of
# modules whose file text has not yet moved to a template in files/ (novox/hq ADR 0349 §2, design 57), or in the
# templates directory its manifest names (`templates`, issue 731). Every other module's file resources name their
# template. This list only shrinks: a module that keeps no file text inline is taken off, and a line the trunk's copy
# does not hold is refused. When it is empty, no manifest in the catalogue carries file text (design 57 package T5,
# issue 658).
#
# A window-manager addition (`contributions`, such as files/i3/*.conf) is not a file resource and takes no template;
# it moves with the collections (design 57 package L2, issue 681) and is not judged here.
+178
View File
@@ -0,0 +1,178 @@
// Command templates refuses a file's text kept inline in a manifest, outside the modules still to move (novox/hq
// ADR 0349 §2, design 57 package T5, issue 658).
//
// A file resource names its text as a template in its module's templates directory (`"template": "<dir>/<name>"`;
// the directory is the manifest's `"templates"`, files/ when it says none), which the module's build packs into an
// archive; the controller reads each template back into the file's content, so the
// mesh holds and composes what it would for the same text inline. A module whose file text has not yet moved is
// listed in the file `inline` beside this check, one per line. Every other module's file resources say no
// `content`; a module on the list that keeps none inline is taken off it; and a line the trunk's copy (the merge
// base's, given with -base) does not hold is refused, so the list only shrinks; while the trunk holds no copy, the
// modules in seed are the trunk's. When it is empty, no manifest in the catalogue carries file text.
//
// Only file resources are judged: a window-manager addition (`contributions`) takes no template and moves with the
// collections (design 57 package L2, issue 681).
//
// go run . [-base <the trunk's inline>] <catalogue root>
//
// Every refusal names the refused module's own templates directory. Exits 1 on a finding.
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
const (
listFile = "inline"
listPathInCatalog = "checks/templates/" + listFile
)
// Check answers, per module that keeps any, the ids of its file resources that say their text inline.
func Check(root string) (map[string][]string, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
out := map[string][]string{}
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Resources []map[string]any `json:"resources"`
}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
for _, r := range m.Resources {
if _, inline := r["content"]; inline && r["type"] == "file" {
out[m.Module] = append(out[m.Module], fmt.Sprint(r["id"]))
}
}
}
return out, nil
}
// TemplatesDir answers the directory a module keeps its templates in, with a trailing slash: its manifest's
// `templates`, files/ when the manifest says none or cannot be read here.
func TemplatesDir(root, module string) string {
dir := "files/"
raw, err := os.ReadFile(filepath.Join(root, "modules", module, "module.json"))
if err != nil {
return dir
}
var m struct {
Templates string `json:"templates"`
}
if json.Unmarshal(raw, &m) == nil && m.Templates != "" {
dir = strings.TrimSuffix(m.Templates, "/") + "/"
}
return dir
}
// Refusal is the finding for a module that keeps file text inline and is not on the list.
func Refusal(module string, ids []string, dir string) string {
return fmt.Sprintf("%s keeps the text of %s inline and is not on %s: keep each file's text in %s and name it as "+
"the file's template, packed by the build's archive from %s (novox/hq ADR 0349 §2)",
module, strings.Join(ids, ", "), listPathInCatalog, dir, dir)
}
// Gained is the finding for a module the list gains against the trunk's copy.
func Gained(module, dir string) string {
return fmt.Sprintf("%s gains %s, which the trunk's copy does not hold: the list only shrinks — move its file text "+
"to %s instead (novox/hq ADR 0349 §2)", listPathInCatalog, module, dir)
}
// seed is the list as it was first written, standing for the trunk's copy until the trunk holds one.
var seed = map[string]bool{"asus-zephyrus-g14": true, "power": true, "showcase": true}
// Listed is the modules a list names, one per line; a line starting with # and a blank line are not modules.
func Listed(path string) (map[string]bool, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
out := map[string]bool{}
s := bufio.NewScanner(f)
for s.Scan() {
line := strings.TrimSpace(s.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
out[line] = true
}
return out, s.Err()
}
// Judge answers the modules that keep file text inline and are not on the list (refused), the modules on the list
// that keep none (stale), and the modules on the list that the trunk's list does not hold (added).
func Judge(found map[string][]string, listed, base map[string]bool) (refused, stale, added []string) {
for m := range found {
if !listed[m] {
refused = append(refused, m)
}
}
for m := range listed {
if _, keeps := found[m]; !keeps {
stale = append(stale, m)
}
if !base[m] {
added = append(added, m)
}
}
sort.Strings(refused)
sort.Strings(stale)
sort.Strings(added)
return refused, stale, added
}
func main() {
basePath := flag.String("base", "", "the trunk's copy of inline (the merge base's); seed when absent")
flag.Parse()
root := "."
if flag.NArg() > 0 {
root = flag.Arg(0)
}
found, err := Check(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
listed, err := Listed(listFile)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
base := seed
if *basePath != "" {
if base, err = Listed(*basePath); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
refused, stale, added := Judge(found, listed, base)
for _, m := range refused {
fmt.Println(Refusal(m, found[m], TemplatesDir(root, m)))
}
for _, m := range stale {
fmt.Printf("%s names %s, which keeps no file text inline: take it off the list\n", listPathInCatalog, m)
}
for _, m := range added {
fmt.Println(Gained(m, TemplatesDir(root, m)))
}
if len(refused) > 0 || len(stale) > 0 || len(added) > 0 {
os.Exit(1)
}
fmt.Printf("templates: no module keeps a file's text inline but the %d listed\n", len(listed))
}
+102
View File
@@ -0,0 +1,102 @@
package main
import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
func module(t *testing.T, root, name, body string) {
t.Helper()
dir := filepath.Join(root, "modules", name)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
// A file resource that says its text inline is found, by module and resource; one naming its template, a
// resource that is not a file and a window-manager addition are not (novox/hq ADR 0349 §2).
func TestEveryInlineFileTextIsFound(t *testing.T) {
root := t.TempDir()
module(t, root, "inline", `{"module":"inline","resources":[{"id":"a","type":"file","content":"x"},`+
`{"id":"b","type":"file","template":"files/b"},{"id":"c","type":"file","content":""}]}`)
module(t, root, "templated", `{"module":"templated","resources":[{"id":"a","type":"file","template":"files/a"},`+
`{"id":"d","type":"directory","path":"/x"}],"contributions":{"i3":[{"content":"bindsym x"}]}}`)
got, err := Check(root)
if err != nil {
t.Fatal(err)
}
want := map[string][]string{"inline": {"a", "c"}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("found %v, want %v", got, want)
}
}
// A module not on the list keeps no file text inline; one on the list that keeps none is taken off it; a line the
// trunk's list does not hold is refused, so the list only shrinks until it is empty.
func TestTheListOfModulesKeepingTextInlineOnlyShrinks(t *testing.T) {
found := map[string][]string{"old": {"a"}, "forgotten": {"b"}}
inline := map[string]bool{"old": true, "done": true, "new": true}
base := map[string]bool{"old": true, "done": true, "forgotten": true}
refused, stale, added := Judge(found, inline, base)
if !reflect.DeepEqual(refused, []string{"forgotten"}) || !reflect.DeepEqual(stale, []string{"done", "new"}) ||
!reflect.DeepEqual(added, []string{"new"}) {
t.Fatalf("refused %v, stale %v, added %v", refused, stale, added)
}
}
func TestTheListIsReadWithoutItsComments(t *testing.T) {
path := filepath.Join(t.TempDir(), "inline")
if err := os.WriteFile(path, []byte("# why\n\npower\n showcase \n"), 0o644); err != nil {
t.Fatal(err)
}
got, err := Listed(path)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, map[string]bool{"power": true, "showcase": true}) {
t.Fatalf("read %v", got)
}
}
// The catalogue itself: no module keeps a file's text inline but those the list names, and the list names none
// that keeps it no longer (design 57 package T5, issue 658: the ten modules without a build moved).
func TestTheCatalogueKeepsFileTextInlineOnlyWhereListed(t *testing.T) {
found, err := Check("../..")
if err != nil {
t.Fatal(err)
}
inline, err := Listed(listFile)
if err != nil {
t.Fatal(err)
}
refused, stale, _ := Judge(found, inline, inline)
if len(refused) > 0 || len(stale) > 0 {
t.Fatalf("keeping file text inline and not listed: %v; listed and keeping none: %v", refused, stale)
}
}
// A refusal names the refused module's own templates directory, not files/ when the module names another (issue 809).
func TestARefusalNamesTheModulesOwnTemplatesDirectory(t *testing.T) {
root := t.TempDir()
module(t, root, "own", `{"module":"own","templates":"t/","resources":[{"id":"a","type":"file","content":"x"}]}`)
module(t, root, "plain", `{"module":"plain","resources":[{"id":"a","type":"file","content":"x"}]}`)
module(t, root, "bare", `{"module":"bare","templates":"templates","resources":[]}`)
for module, dir := range map[string]string{"own": "t/", "plain": "files/", "bare": "templates/"} {
if got := TemplatesDir(root, module); got != dir {
t.Fatalf("%s: directory %q, want %q", module, got, dir)
}
}
msg := Refusal("own", []string{"a"}, TemplatesDir(root, "own"))
if !strings.Contains(msg, "t/") || strings.Contains(msg, "files/") {
t.Fatalf("refusal %q", msg)
}
if msg := Gained("own", TemplatesDir(root, "own")); !strings.Contains(msg, "t/") || strings.Contains(msg, "files/") {
t.Fatalf("gained %q", msg)
}
}
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/toolchains
go 1.22
+105
View File
@@ -0,0 +1,105 @@
// Command toolchains refuses any manifest but the owner's declaring a language's toolchain package (novox/hq
// ADR 0477 clause 4, issue 1573): the package go only in modules/go, nodejs and npm only in modules/nodejs, and buildah (issue 1702) only in modules/buildah.
// Everything that compiles or runs that language requires the owner's provision instead (go-toolchain,
// node-toolchain), so the package is declared once, and a node without the owner refuses the module that
// needs it at resolution rather than failing at its first build. The owners must declare their packages.
//
// go run . <catalogue root>
//
// Exits 1 on a finding.
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
)
// Owners names, for each toolchain package, the one module that declares it.
var Owners = map[string]string{
"buildah": "buildah",
"go": "go",
"nodejs": "nodejs",
"npm": "nodejs",
}
// Finding is one refusal: Module declares Package, which Owner alone may, or (Missing) Owner does not declare
// the Package it owns.
type Finding struct {
Module, Package, Owner string
Missing bool
}
// Check reads every manifest under root/modules and answers what it refuses.
func Check(root string) ([]Finding, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
var out []Finding
declared := map[string]map[string]bool{} // module -> packages
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Resources []struct {
Type string `json:"type"`
Package string `json:"package"`
} `json:"resources"`
}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
declared[m.Module] = map[string]bool{}
for _, r := range m.Resources {
if r.Type != "package" {
continue
}
declared[m.Module][r.Package] = true
if owner, ok := Owners[r.Package]; ok && owner != m.Module {
out = append(out, Finding{Module: m.Module, Package: r.Package, Owner: owner})
}
}
}
var pkgs []string
for p := range Owners {
pkgs = append(pkgs, p)
}
sort.Strings(pkgs)
for _, p := range pkgs {
if !declared[Owners[p]][p] {
out = append(out, Finding{Module: Owners[p], Package: p, Owner: Owners[p], Missing: true})
}
}
return out, nil
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
found, err := Check(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
for _, f := range found {
if f.Missing {
fmt.Printf("%s does not declare the package %s it owns (novox/hq ADR 0477 clause 4)\n", f.Owner, f.Package)
continue
}
fmt.Printf("%s declares the package %s: only %s may; require its provision instead (novox/hq ADR 0477 clause 4)\n",
f.Module, f.Package, f.Owner)
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("toolchains: go, nodejs, npm and buildah are declared only by their owners")
}
+82
View File
@@ -0,0 +1,82 @@
package main
import (
"os"
"path/filepath"
"reflect"
"testing"
)
func catalogue(t *testing.T, manifests map[string]string) string {
t.Helper()
root := t.TempDir()
for name, body := range manifests {
dir := filepath.Join(root, "modules", name)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
return root
}
func TestOnlyTheOwnerDeclaresAToolchainPackage(t *testing.T) {
root := catalogue(t, map[string]string{
"go": `{"module":"go","resources":[{"id":"go","type":"package","package":"go"}]}`,
"nodejs": `{"module":"nodejs","resources":[{"id":"nodejs","type":"package","package":"nodejs"},{"id":"npm","type":"package","package":"npm"}]}`,
"buildah": `{"module":"buildah","resources":[{"id":"b","type":"package","package":"buildah"}]}`,
"lab": `{"module":"lab","resources":[{"id":"git","type":"package","package":"git"},{"id":"go","type":"package","package":"go"}]}`,
"builder": `{"module":"builder","resources":[{"id":"n","type":"package","package":"nodejs"},{"id":"dir","type":"directory","path":"/go"}]}`,
})
got, err := Check(root)
if err != nil {
t.Fatal(err)
}
want := []Finding{{Module: "builder", Package: "nodejs", Owner: "nodejs"}, {Module: "lab", Package: "go", Owner: "go"}}
if !reflect.DeepEqual(got, want) {
t.Errorf("found %v, want %v", got, want)
}
}
func TestASecondDeclarerOfBuildahIsRefused(t *testing.T) {
root := catalogue(t, map[string]string{
"go": `{"module":"go","resources":[{"id":"go","type":"package","package":"go"}]}`,
"nodejs": `{"module":"nodejs","resources":[{"id":"nodejs","type":"package","package":"nodejs"},{"id":"npm","type":"package","package":"npm"}]}`,
"buildah": `{"module":"buildah","resources":[{"id":"b","type":"package","package":"buildah"}]}`,
"imagery": `{"module":"imagery","resources":[{"id":"b","type":"package","package":"buildah"}]}`,
})
got, err := Check(root)
if err != nil {
t.Fatal(err)
}
want := []Finding{{Module: "imagery", Package: "buildah", Owner: "buildah"}}
if !reflect.DeepEqual(got, want) {
t.Errorf("found %v, want %v", got, want)
}
}
func TestTheOwnersMustExistAndDeclareTheirPackages(t *testing.T) {
root := catalogue(t, map[string]string{
"go": `{"module":"go","resources":[]}`,
})
got, err := Check(root)
if err != nil {
t.Fatal(err)
}
if len(got) != 4 {
t.Errorf("an owner missing its packages gave %v, want four findings (buildah, go, nodejs, npm)", got)
}
}
// The catalogue as it stands: this is the check itself, run here too.
func TestTheCatalogueGivesEachToolchainPackageToItsOwnerAlone(t *testing.T) {
got, err := Check("../..")
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Errorf("the catalogue declares a toolchain package outside its owner: %v", got)
}
}
+73
View File
@@ -0,0 +1,73 @@
package main
import (
"fmt"
"regexp"
"sort"
"strings"
)
// The daemons' lines (novox/hq ADR 0272, its insight of 2026-10-09): the operator types one command, `nox`
// (`mesh-cli`). The node-engine (`nox-mesh-host`) and the controller (`mesh-controller`) are daemons, and a
// person never types their names — so no text a module can show names a line of theirs. The list is the
// copy in retired-lines at the catalogue's root, which novox/hq's words.py prints (`--list lines`) and
// compares; one line each, `<daemon> <command>`, and a trailing ` …` for a command that is a line only with
// an argument after it (`mesh-controller module check` is a line; "the mesh-controller module" is prose).
// retiredLine is one daemon's line: the daemon, its command, and whether an argument must follow.
type retiredLine struct {
daemon, command string
withArgument bool
}
// readLines reads retired-lines: one line a line; blank lines and lines starting with # are not lines.
func readLines(path string) ([]retiredLine, error) {
entries, err := readList(path)
if err != nil {
return nil, err
}
var out []retiredLine
for _, e := range entries {
withArgument := strings.HasSuffix(e, " …")
e = strings.TrimSuffix(e, " …")
parts := strings.Fields(e)
if len(parts) != 2 {
return nil, fmt.Errorf("retired-lines: %q is not `<daemon> <command>`", e)
}
out = append(out, retiredLine{daemon: parts[0], command: parts[1], withArgument: withArgument})
}
return out, nil
}
// linePattern matches the line whole: the daemon, white space, the command. A command that is a line only
// with an argument is matched as a line is written — at the start of a line or a code span, a prompt allowed,
// with an argument after it — so prose that names the daemon beside the word ("the mesh-controller module is
// assigned") is not one.
func linePattern(l retiredLine) *regexp.Regexp {
daemon, command := regexp.QuoteMeta(l.daemon), regexp.QuoteMeta(l.command)
if l.withArgument {
tick := "`"
return regexp.MustCompile(`(?m)(?:^|` + tick + `)[ \t]*(?:\$[ \t]*)?(` + daemon + `\s+` + command + `\s+[^\s` + tick + `])`)
}
return regexp.MustCompile(`(?:^|[^\w-])(` + daemon + `\s+` + command + `)(?:$|[^\w-])`)
}
// checkLines finds every daemon's line in the texts.
func checkLines(lines []retiredLine, texts []text) []string {
var out []string
for _, l := range lines {
rx := linePattern(l)
for _, t := range texts {
if m := rx.FindStringSubmatchIndex(t.s); m != nil {
line := t.line
if line > 0 {
line += strings.Count(t.s[:m[2]], "\n")
}
out = append(out, fmt.Sprintf("%s:%d: `%s %s` is a daemon's line — the operator types `nox …` (novox/hq "+
"ADR 0272) — in %q", t.file, line, l.daemon, l.command, excerpt(t.s, m[2], m[3])))
}
}
}
sort.Strings(out)
return out
}
+23 -2
View File
@@ -2,7 +2,9 @@
// 0244): no word the glossary retired for the tools' descriptions — the copy in retired-words at the
// catalogue's root — in any text a module's tools can show. That text is every string literal in a module's
// own code that is not a test (a tool's description, the notes and errors it answers with) and every
// description in its manifest. Comments are not read: an agent never sees them.
// description in its manifest. Comments are not read: an agent never sees them. And no daemon's line
// (novox/hq ADR 0272, lines.go): the copy in retired-lines, the lines a person would otherwise be told to
// type of the node-engine's or the controller's binary, where the operator's command is `nox`.
//
// A word is matched whole and in any case, a space in it matching any run of white space, and its plural
// (`s` or `es` on its last part) is matched as the word. A vendor word is
@@ -97,7 +99,26 @@ func main() {
fmt.Printf("words: %d use(s) of a word the glossary retired (novox/hq ADR 0244) — say it in the glossary's word\n", len(findings))
os.Exit(1)
}
fmt.Printf("words: %d retired words, none in the %d strings of %d files\n", len(words), len(texts), files)
// And no daemon's line (novox/hq ADR 0272): what a module tells the operator to type is a `nox` line.
lines, err := readLines(filepath.Join(root, "retired-lines"))
if err != nil {
fmt.Fprintln(os.Stderr, "words:", err)
os.Exit(2)
}
if len(lines) == 0 {
fmt.Fprintln(os.Stderr, "words: retired-lines lists no line — the check would pass on anything")
os.Exit(2)
}
if findings := checkLines(lines, texts); len(findings) > 0 {
for _, f := range findings {
fmt.Println(f)
}
fmt.Printf("words: %d daemon's line(s) in what a module shows — the operator types one command, nox (novox/hq ADR 0272)\n",
len(findings))
os.Exit(1)
}
fmt.Printf("words: %d retired words and %d daemons' lines, none in the %d strings of %d files\n", len(words), len(lines),
len(texts), files)
}
// readList reads retired-words: one word or phrase a line; blank lines and lines starting with # are not words.
+64 -1
View File
@@ -1,6 +1,9 @@
package main
import "testing"
import (
"os"
"testing"
)
// A retired word's plural is the word; anything else joined on is another word.
func TestPatternMatchesTheWordAndItsPlural(t *testing.T) {
@@ -24,3 +27,63 @@ func TestPatternMatchesTheWordAndItsPlural(t *testing.T) {
}
}
}
// A daemon's line is found whole — the daemon, its command, and an argument where the command is a line only with
// one — and prose that merely names the daemon beside a word is not one (novox/hq ADR 0272).
func TestADaemonsLineIsFoundWhole(t *testing.T) {
push := retiredLine{daemon: "mesh-controller", command: "push"}
module := retiredLine{daemon: "mesh-controller", command: "module", withArgument: true}
handOver := retiredLine{daemon: "nox-mesh-host", command: "hand-over"}
cases := []struct {
line retiredLine
text string
want bool
}{
{push, "run `mesh-controller push laptop` after", true},
{push, "mesh-controller push", true},
{push, "mesh-controller\n push", true},
{push, "mesh-controller pushed it", false},
{push, "the mesh-controller pushes", false},
{push, "mesh-controller-push", false},
{module, "`mesh-controller module check modules/*/module.json`", true},
{module, "run\n $ mesh-controller module check x", true},
{module, "the mesh-controller module is assigned", false},
{module, "mesh-controller module", false},
{module, "`mesh-controller module`", false},
{handOver, "`nox-mesh-host hand-over /srv/x` at the machine", true},
{handOver, "`nox node hand-over laptop /srv/x`", false},
{retiredLine{daemon: "mesh-host", command: "hand-over"}, "nox-mesh-host hand-over", false},
}
for _, c := range cases {
if got := linePattern(c.line).MatchString(c.text); got != c.want {
t.Errorf("%s %s on %q = %v, want %v", c.line.daemon, c.line.command, c.text, got, c.want)
}
}
found := checkLines([]retiredLine{push, module}, []text{{file: "m/a.go", line: 3, s: "say\n`mesh-controller push`"},
{file: "m/b.go", line: 1, s: "the mesh-controller module"}})
if len(found) != 1 || found[0] != "m/a.go:4: `mesh-controller push` is a daemon's line — the operator types `nox …` (novox/hq ADR 0272) — in \"say `mesh-controller push`\"" {
t.Fatalf("found %q", found)
}
}
// The list's lines read as `<daemon> <command>`, a trailing ` …` meaning an argument must follow; anything else is
// refused rather than read as some line.
func TestRetiredLinesAreReadAsDaemonAndCommand(t *testing.T) {
dir := t.TempDir()
path := dir + "/retired-lines"
if err := os.WriteFile(path, []byte("# the list\nmesh-controller push\nmesh-controller module …\n\nnox-mesh-host hand-over\n"), 0o644); err != nil {
t.Fatal(err)
}
lines, err := readLines(path)
if err != nil || len(lines) != 3 || lines[0] != (retiredLine{daemon: "mesh-controller", command: "push"}) ||
lines[1] != (retiredLine{daemon: "mesh-controller", command: "module", withArgument: true}) ||
lines[2] != (retiredLine{daemon: "nox-mesh-host", command: "hand-over"}) {
t.Fatalf("read %+v, %v", lines, err)
}
if err := os.WriteFile(path, []byte("mesh-controller\n"), 0o644); err != nil {
t.Fatal(err)
}
if _, err := readLines(path); err == nil {
t.Fatal("a line with no command was read")
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
70
58
The long-running resources of this catalogue that do not say how they are ready (`health`, novox/hq ADR 0240
rule 8), as `module check` counts them. It may only go down: merge-check.sh fails a change that raises it, and
one that lowers it writes the new number on the first line. From 2026-11-18, or once it is 0, a long-running
+140
View File
@@ -0,0 +1,140 @@
#!/bin/sh
# The catalogue's TypeScript part of its own check (novox/hq issue 302's `mesh-check-also`), declared in
# merge-check.sh's header and run by the build seat in the TypeScript toolchain, after the Go part. The media
# catalogue's twin is issue 311. Before this, a TypeScript module's change ran nothing of its own before it
# merged: the Go part said "NOT TESTED HERE", and a type error in the forge module would first have shown in
# its walk.
#
# For every touched module that has a package.json, in a copy of it outside the clone:
#
# 1. its dependencies are the toolchain's own (/app/node_modules: the mesh's SDK, Node's types and what the
# runtime carries), copied beside it, never fetched: the check holds no credential for the package
# registry. A module that depends on a package the toolchain does not hold **fails the check**, by name:
# it could not be type-checked or tested, and what could not run is a failure, never a note (novox/hq
# issue 385);
# 2. every source that is not a test is type-checked with the toolchain's compiler and the flags a build
# compiles with — the same check the build makes, before the merge rather than in the walk;
# 3. its tests (test/*.test.ts) run under Node's own runner with types stripped, as its own `npm test` does
# after its build: the sources are compiled to dist/ and beside themselves first, so a test that imports
# the compiled module, or a source that imports its sibling by its .js name, finds it.
#
# Said, never passed silently: a module with no tests is said NOT TESTED on every run that touches it —
# nothing of it could fail. A module whose tests exist and could not run fails (novox/hq issue 385).
#
# By hand: MESH_CHECK_MODULES=gitea sh merge-check-typescript.sh, in the toolchain image (it reads
# /app/node_modules), or with TOOLCHAIN_MODULES pointing at a node_modules that holds the same.
#
# **In an agent's session, outside the toolchain image** (novox/hq issue 742), the same node_modules is
# assembled first, in a throwaway directory under the check's own workspace, the way the image's `deps` stage
# assembles it: the toolchain's own package.json and lock (mesh-tools' node-tools/, read from the forge this
# clone came from, so the list is never copied here) installed with `npm ci`, then the SDK the mesh last
# published (`@novox/mesh-sdk@latest`), the @novox scope from the forge's package registry, read without a
# credential. Node and npm are the dev-tools module's; nothing is installed on the machine, no npm
# configuration or cache of the account's is read or written. Only when neither TOOLCHAIN_MODULES nor
# /app/node_modules is there: the forge's check, in the image, fetches nothing, as before. The forge is
# MESH_FORGE (default: the origin remote's), the owner MESH_FORGE_OWNER (default: the origin's owner).
set -eu
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
deps="${TOOLCHAIN_MODULES:-/app/node_modules}"
if [ -z "${TOOLCHAIN_MODULES:-}" ] && [ ! -f "$deps/typescript/bin/tsc" ]; then
origin=$(git remote get-url origin 2>/dev/null || true)
forge="${MESH_FORGE:-$(printf '%s\n' "$origin" | sed -n 's#^\(https\{0,1\}://[^/]*\)/.*#\1#p')}"
owner="${MESH_FORGE_OWNER:-$(printf '%s\n' "$origin" | sed -n 's#^https\{0,1\}://[^/]*/\([^/]*\)/.*#\1#p')}"
if [ -z "$forge" ] || [ -z "$owner" ]; then
echo "NOT CHECKED: no TypeScript toolchain at $deps, and no forge to assemble one from (origin is '$origin'; set MESH_FORGE and MESH_FORGE_OWNER, or TOOLCHAIN_MODULES)"
exit 1
fi
echo "no TypeScript toolchain at $deps: assembling one from $forge/$owner/mesh-tools node-tools/ and $owner's package registry (novox/hq issue 742)"
tc="$work/toolchain"
mkdir -p "$tc"
printf '@%s:registry=%s/api/packages/%s/npm/\n' "$owner" "$forge" "$owner" >"$work/npmrc"
# Read with Node's own fetch (dev-tools declares Node, not curl); a status that is not 2xx fails.
if ! { node -e '
const [base, dir] = process.argv.slice(1);
Promise.all(["package.json", "package-lock.json"].map(async (f) => {
const r = await fetch(base + f);
if (!r.ok) throw new Error(base + f + ": " + r.status);
require("fs").writeFileSync(dir + "/" + f, await r.text());
})).catch((e) => { console.error(e.message); process.exit(1); });' \
"$forge/$owner/mesh-tools/raw/branch/main/node-tools/" "$tc" &&
(cd "$tc" && export npm_config_userconfig="$work/npmrc" npm_config_cache="$work/npm-cache" &&
npm ci --ignore-scripts --no-audit --no-fund --loglevel=error &&
npm install --ignore-scripts --no-audit --no-fund --no-save --loglevel=error "@$owner/mesh-sdk@latest"); }; then
echo "NOT CHECKED: the TypeScript toolchain could not be assembled from $forge (novox/hq issue 742)"
exit 1
fi
deps="$tc/node_modules"
fi
tsc="$deps/typescript/bin/tsc"
if [ ! -f "$tsc" ]; then
echo "NOT CHECKED: no TypeScript compiler at $tsc — this part runs in the TypeScript toolchain"
exit 1
fi
# The modules the change reaches: the planner's answer (hq ADR 0238), and every module a changed file is in —
# a change to a module's tests alone moves no build (hq ADR 0267), and its tests must still run.
touched=$({
printf '%s\n' "${MESH_CHECK_MODULES:-}" | tr ',' '\n' | sed -e 's#^modules/##' -e '/\//d'
printf '%s\n' "${MESH_CHECK_CHANGED:-}" | tr ',' '\n' | sed -n 's#^modules/\([^/]*\)/.*#\1#p'
} | sed '/^$/d' | sort -u)
failed=""
for m in $touched; do
d="modules/$m"
[ -f "$d/package.json" ] || continue
copy="$work/$m"
mkdir -p "$copy"
(cd "$d" && tar --exclude=./node_modules --exclude=./dist -cf - .) | (cd "$copy" && tar -xf -)
# What it depends on, against what the toolchain holds.
missing=$(node -e '
const p = require(process.argv[1]); const have = require("fs").readdirSync(process.argv[2]);
const scoped = (n) => n.startsWith("@") ? n.split("/")[0] : n;
const names = Object.keys({ ...(p.dependencies || {}), ...(p.devDependencies || {}) });
const held = (n) => have.includes(n) || (n.startsWith("@") && have.includes(scoped(n)) &&
require("fs").existsSync(process.argv[2] + "/" + n));
console.log(names.filter((n) => n !== "typescript" && !held(n)).join(" "));' "$copy/package.json" "$deps")
if [ -n "$missing" ]; then
echo "FAILED: $d could not be checked — it depends on $missing, which the toolchain does not hold and the check cannot fetch (novox/hq issue 385)"
failed="$failed $m"
continue
fi
mkdir -p "$copy/node_modules"
for p in "$deps"/*; do
case "$(basename "$p")" in typescript | esbuild | @esbuild) continue ;; esac
cp -r "$p" "$copy/node_modules/"
done
sources=$(cd "$copy" && find . -name '*.ts' ! -name '*.d.ts' ! -path './test/*' ! -path './node_modules/*' ! -path './dist/*' | sort)
if [ -n "$sources" ]; then
echo "type-checking $d (TypeScript)"
# shellcheck disable=SC2086 # the module's sources, none with a space in its name
if ! (cd "$copy" && node "$tsc" --noEmit --module NodeNext --moduleResolution NodeNext --target ES2022 \
--types node $sources); then
failed="$failed $m"
continue
fi
else
echo "$d has no TypeScript sources of its own to type-check: its tests read its files"
fi
if ! ls "$copy"/test/*.test.ts >/dev/null 2>&1; then
echo "NOT TESTED: $d is TypeScript and has no tests of its own"
continue
fi
if [ -n "$sources" ]; then
# shellcheck disable=SC2086
(cd "$copy" && node "$tsc" --module NodeNext --moduleResolution NodeNext --target ES2022 --types node \
--rootDir . --outDir dist $sources && node "$tsc" --module NodeNext --moduleResolution NodeNext \
--target ES2022 --types node --rootDir . --outDir . $sources)
fi
echo "testing $d (TypeScript)"
if ! (cd "$copy" && node --test --test-reporter=spec --experimental-strip-types \
--no-warnings=ExperimentalWarning test/*.test.ts); then
failed="$failed $m"
fi
done
if [ -n "$failed" ]; then
echo "TypeScript modules that failed:$failed"
exit 1
fi
+133 -7
View File
@@ -1,5 +1,6 @@
#!/bin/sh
# mesh-check-toolchain: go
# mesh-check-also: typescript merge-check-typescript.sh
#
# The catalogue's own check (novox/hq ADR 0237 as amended): the second layer of a pull request's merge
# check, `mesh/repo-check`, run by the build seat in the mesh's Go toolchain.
@@ -13,8 +14,13 @@
# it shares a rule with refused (MESH_GATE is the controller the mesh runs) — and the count of
# long-running resources without `health` held to the number in health-undeclared, which only goes down;
# 2. the Go tests of every module the change touches that has them, under the race detector when the
# toolchain has a C compiler — and a module whose dependencies cannot be fetched here, or that is
# written in TypeScript, is said as not tested, never passed silently;
# toolchain has a C compiler. A requirement of the mesh's own forge the module cache does not hold (a
# pinned SDK commit) is taken from its repository's clone beside the check (checks/deps); a module whose
# dependencies still cannot be had **fails the check**, by name and why — its tests could not run, and a
# test that could not run is a failure, never a note (novox/hq issue 385). The integration tests (`//go:build
# integration`) run here, with -tags integration, and only here. A module written in
# TypeScript is type-checked and tested by
# merge-check-typescript.sh, declared above, in the TypeScript toolchain (novox/hq issues 302, 311);
# 3. the words (novox/hq ADR 0244): no word the glossary retired for the tools, as copied in
# retired-words, in any text a module's tools can show an agent — every string in its own code that is
# not a test, and every description in its manifest. Run over every module, not only the touched ones,
@@ -22,9 +28,100 @@
# 4. the paths (novox/hq ADR 0260): no module names a path under /run, /var/lib, /usr/local/lib or /etc
# that another module's manifest places — modules talk over the bus, not through each other's files.
# Over every module; checks/paths/shared and checks/paths/known narrow it, and known only shrinks.
# 5. the agent's home (novox/hq ADR 0266): no module unpacks an archive, or writes into a file, under
# ${machine:agent-home} — the node-engine does both after checking the path, and the agent account owns
# that home and could swap a link in between.
# 6. the secrets (novox/hq issue 342): no mergeable file fills a key with a vault secret that a setting could
# replace — the key is `protected`, or left out of the file's `takes-settings`. checks/secrets/known names
# the nested ones the controller cannot protect alone yet, and only shrinks.
# 7. the list of predefined keys (novox/hq ADR 0348 clause 7): a change to predefined.json removes no key a
# module still names, judged against the trunk's copy at the merge base. Whether a removed key still holds a
# value at the mesh or a node is mesh-config's to refuse; this check reads no store. The list itself and
# every manifest against it are judged by the controller's module check in step 1.
# 8. the config form (novox/hq ADR 0348 clauses 1 and 11): no module says `settings`, `takes-settings` or
# `${setting:<key>}`, in its manifest or a template it names, or has a mergeable file naming no keys (which
# takes its whole layer), unless checks/config/unmoved lists it as not yet moved; that list only shrinks,
# judged against the trunk's copy at the merge base.
# 9. the file text (novox/hq ADR 0349 §2): no module keeps a file's text inline in its manifest (`content`)
# unless checks/templates/inline lists it as not yet moved; each names its template in files/. That list
# only shrinks, judged against the trunk's copy at the merge base.
# 10. the mail-store seat (novox/hq ADR 0378 §3, issue 800): no manifest but modules/mesh-mail claims it.
# 11. owned paths (novox/hq ADR 0422 §8, issue 1024; it holds ~/.ssh for ssh-client as ADR 0415 §6 asked):
# every directory and archive says `unknown` (move-out, module or program) and a valid `knows`; `move-out`
# only under a home; and no module declares a path inside a directory of another module.
# checks/sshdir (ADR 0415 §6) stays while main runs it; it goes under issue 1035 once main runs checks/owned.
# 12. the image builder (novox/hq ADR 0487 §3, issue 1704): only modules/buildah provides rootless-image-build.
# 13. the image's place (novox/hq ADR 0487 §1, issue 1707): every image builds from a Dockerfile in its own module's directory.
set -eu
(cd checks/words && go test -count=1 . && go run . ../..)
# Node names (novox/hq ADR 0507 §9, issue 1838): no node name of this mesh in a covered module; the names are in
# `particulars`, the covered modules in checks/particulars/covered.
(cd checks/particulars && go test -count=1 . && go run . ../..)
# The toolchains (novox/hq ADR 0477 clause 4): the package go only in modules/go, nodejs and npm only in modules/nodejs.
(cd checks/toolchains && go test -count=1 . && go run . ../..)
# The image builder (novox/hq ADR 0487 §3): only modules/buildah provides rootless-image-build, docker never.
(cd checks/imagebuild && go test -count=1 . && go run . ../..)
# The image's place (novox/hq ADR 0487 §1): every image builds from a Dockerfile in its own module's directory.
(cd checks/imageplace && go test -count=1 . && go run . ../..)
# The agent's home (novox/hq ADR 0266): no archive, and no file written into, under it.
(cd checks/agenthome && go test -count=1 . && go run . ../..)
# The mail-store seat (novox/hq ADR 0378 §3): only mesh-mail may claim it.
(cd checks/mailstore && go test -count=1 . && go run . ../..)
# ~/.ssh (novox/hq ADR 0415 §6): only ssh-client may name a path under it. Main's merge check runs this line, so
# checks/sshdir stays until main runs checks/owned; it goes under issue 1035.
(cd checks/sshdir && go test -count=1 . && go run . ../..)
# Owned paths (novox/hq ADR 0422 §8): unknown files named per directory; no path inside another module's directory.
(cd checks/owned && go test -count=1 . && go run . ../..)
# The secrets (novox/hq issue 342): a password is not a setting, so no setting can replace the vault's. The
# known list only shrinks: judged against the trunk's copy at the merge base, a line it does not hold is refused.
secrets_base=""
secrets_ref="origin/${MESH_CHECK_BASE:-main}"
if merge_base=$(git merge-base HEAD "$secrets_ref" 2>/dev/null); then
if git cat-file -e "$merge_base:checks/secrets/known" 2>/dev/null; then
secrets_base=$(mktemp)
git show "$merge_base:checks/secrets/known" >"$secrets_base"
fi
else
echo "NOT COMPARED WITH THE TRUNK: no merge base with $secrets_ref; checks/secrets/known is held to the list it was first written with"
fi
(cd checks/secrets && go test -count=1 . && if [ -n "$secrets_base" ]; then go run . -base "$secrets_base" ../..; else go run . ../..; fi)
# The list of predefined keys (novox/hq ADR 0348 clause 7): no key removed that a module still names. The trunk's
# list at the merge base is what it is judged against; a trunk without the file had none.
predefined_base=""
if merge_base=$(git merge-base HEAD "origin/${MESH_CHECK_BASE:-main}" 2>/dev/null); then
predefined_base=$(mktemp)
if ! git show "$merge_base:predefined.json" >"$predefined_base" 2>/dev/null; then
echo '{}' >"$predefined_base"
fi
else
echo "NOT COMPARED WITH THE TRUNK: no merge base with origin/${MESH_CHECK_BASE:-main}; no removal from predefined.json can be seen"
fi
(cd checks/predefined && go test -count=1 . && if [ -n "$predefined_base" ]; then go run . -base "$predefined_base" ../..; else go run . ../..; fi)
# The config form (novox/hq ADR 0348 §1, §11): a moved module says no old name, and the list of modules not yet moved
# only shrinks, judged against the trunk's copy at the merge base; a trunk without one is the list's first copy.
unmoved_base=""
if merge_base=$(git merge-base HEAD "origin/${MESH_CHECK_BASE:-main}" 2>/dev/null); then
if git cat-file -e "$merge_base:checks/config/unmoved" 2>/dev/null; then
unmoved_base=$(mktemp)
git show "$merge_base:checks/config/unmoved" >"$unmoved_base"
fi
else
echo "NOT COMPARED WITH THE TRUNK: no merge base with origin/${MESH_CHECK_BASE:-main}; checks/config/unmoved is held to the list it was first written with"
fi
(cd checks/config && go test -count=1 . && if [ -n "$unmoved_base" ]; then go run . -base "$unmoved_base" ../..; else go run . ../..; fi)
# The file text (novox/hq ADR 0349 §2): a file's text is a template in files/, outside the modules not yet moved; the
# list of those only shrinks, judged against the trunk's copy at the merge base, a trunk without one being its first.
inline_base=""
if merge_base=$(git merge-base HEAD "origin/${MESH_CHECK_BASE:-main}" 2>/dev/null); then
if git cat-file -e "$merge_base:checks/templates/inline" 2>/dev/null; then
inline_base=$(mktemp)
git show "$merge_base:checks/templates/inline" >"$inline_base"
fi
else
echo "NOT COMPARED WITH THE TRUNK: no merge base with origin/${MESH_CHECK_BASE:-main}; checks/templates/inline is held to the list it was first written with"
fi
(cd checks/templates && go test -count=1 . && if [ -n "$inline_base" ]; then go run . -base "$inline_base" ../..; else go run . ../..; fi)
# The lists that narrow the paths check may not grow unseen: the base branch's copies are what they are
# judged against (MESH_CHECK_BASE, main when unset), read from the clone the check runs in.
base_lists=""
@@ -70,16 +167,26 @@ if [ -n "${MESH_CHECK_MODULES:-}" ]; then
else
touched=$(printf '%s\n' "${MESH_CHECK_CHANGED:-}" | tr ',' '\n' | sed -n 's#^modules/\([^/]*\)/.*#\1#p' | sort -u)
fi
# A module's dependencies: fetched, or a forge requirement taken from its clone beside the check
# (MESH_CHECK_BESIDE); its tests run against them (checks/deps, novox/hq issue 385). The helper's own test
# runs first.
(cd checks/deps && go test -count=1 .)
deps=$(mktemp -d)
trap 'rm -rf "$deps"' EXIT
(cd checks/deps && go build -o "$deps/deps" .)
untestable=""
for m in $touched; do
[ -d "modules/$m" ] || continue
if [ ! -f "modules/$m/go.mod" ]; then
[ -f "modules/$m/package.json" ] && echo "NOT TESTED HERE: modules/$m is TypeScript; the gate judges its manifest"
[ -f "modules/$m/package.json" ] && echo "modules/$m is TypeScript: merge-check-typescript.sh type-checks and tests it"
continue
fi
if ! (cd "modules/$m" && GOPRIVATE=git.novox.be go mod download >/dev/null 2>&1); then
echo "NOT TESTED: modules/$m — its dependencies cannot be fetched by the build seat"
if ! modfile=$(GOPRIVATE=git.novox.be "$deps/deps" -beside "${MESH_CHECK_BESIDE:-}" -work "$deps" "modules/$m"); then
echo "FAILED: modules/$m could not be tested — $modfile"
untestable="$untestable modules/$m"
continue
fi
[ -n "$modfile" ] && echo "modules/$m takes its forge dependencies from the clones beside the check"
echo "testing modules/$m"
unformatted=$(cd "modules/$m" && gofmt -l .)
if [ -n "$unformatted" ]; then
@@ -88,6 +195,25 @@ for m in $touched; do
fi
cgo=0
[ -n "$race" ] && cgo=1
(cd "modules/$m" && CGO_ENABLED=$cgo GOPRIVATE=git.novox.be go vet ./... &&
CGO_ENABLED=$cgo GOPRIVATE=git.novox.be go test $race -count=1 ./...)
# The integration tests (a file whose first line is `//go:build integration`: a test that starts a bus, a store or
# another process) run here and nowhere else: the unit suite stays under a second (novox/hq playbook 07).
(cd "modules/$m" && CGO_ENABLED=$cgo GOPRIVATE=git.novox.be go vet -tags integration ${modfile:+"-modfile=$modfile"} ./... &&
CGO_ENABLED=$cgo GOPRIVATE=git.novox.be go test -tags integration ${modfile:+"-modfile=$modfile"} $race -count=1 ./...)
# The docker tests of the bus's snapshot program and entrypoint (novox/hq ADR 0480 clause 6) start throwaway nats
# containers, and the Go toolchain container holds no docker, so above they skip with a "NOT RUN" line. Where the
# build seat says docker exists (MESH_CHECK_DOCKER=1) they run here by name, and a missing docker fails the check
# (MESH_REQUIRE_DOCKER=1 makes the tests fail instead of skip). Without it the step says it did not run.
if [ "$m" = nats ]; then
if [ "${MESH_CHECK_DOCKER:-}" = 1 ]; then
(cd modules/nats && MESH_REQUIRE_DOCKER=1 CGO_ENABLED=$cgo GOPRIVATE=git.novox.be go test -tags integration ${modfile:+"-modfile=$modfile"} -count=1 \
-run 'TestTheDeclaredDumpRunsInTheBussImage|TestTheEntrypointReloadsTheServerWithoutDroppingAConnection|TestTheStoreSurvivesTheNewDeclarationTheOldAndTheNewAgain|TestASnapshotOfALiveBusRestoresToTheSameContent|TestTheSnapshotUserCannotChangeTheBus' \
./cmd/mesh-nats-snapshot) || { echo "the bus's docker tests failed"; exit 1; }
else
echo "NOT RUN: the bus's five docker tests (modules/nats/cmd/mesh-nats-snapshot) skipped: this check has no docker (MESH_CHECK_DOCKER is not 1)"
fi
fi
done
if [ -n "$untestable" ]; then
echo "the check fails: the tests of$untestable could not run (novox/hq issue 385)"
exit 1
fi
+4 -4
View File
@@ -22,11 +22,11 @@ for GTK, Qt, the portal and the cursor, dark by default. It claims no seat, beca
| path | class | from |
|---|---|---|
| `~/.config/gtk-3.0/settings.ini` | owned (the found file kept once) | [`config/gtk-settings.ini`](config/gtk-settings.ini) |
| `~/.config/gtk-3.0/settings.ini` | owned (the found file kept once) | [`files/gtk-settings.ini`](files/gtk-settings.ini) |
| `~/.config/gtk-4.0/settings.ini` | owned | the same file |
| `~/.config/qt6ct/qt6ct.conf` | owned | [`config/qt6ct.conf`](config/qt6ct.conf) |
| `~/.config/xdg-desktop-portal/portals.conf` | owned | [`config/portals.conf`](config/portals.conf) |
| `~/.icons/default/index.theme` | owned | [`config/cursor-index.theme`](config/cursor-index.theme): the cursor for programs that read neither `XCURSOR_THEME` nor the resources |
| `~/.config/qt6ct/qt6ct.conf` | owned | [`files/qt6ct.conf`](files/qt6ct.conf) |
| `~/.config/xdg-desktop-portal/portals.conf` | owned | [`files/portals.conf`](files/portals.conf) |
| `~/.icons/default/index.theme` | owned | [`files/cursor-index.theme`](files/cursor-index.theme): the cursor for programs that read neither `XCURSOR_THEME` nor the resources |
**`qt6ct.conf` is the mesh's now.** A change made in qt6ct's own window is replaced at the next push,
and the window's saved geometry goes with it. The theme is this module's to say. Settings will make it
@@ -18,10 +18,18 @@ type manifest struct {
Environment struct {
Variables map[string]string `json:"variables"`
} `json:"environment"`
Items []struct {
Collection string `json:"collection"`
Attributes map[string]string `json:"attributes"`
Template string `json:"template"`
} `json:"items"`
Shell []struct {
For, Slot, Code string
} `json:"shell"`
Resources []map[string]any `json:"resources"`
Build struct {
Artifacts []map[string]any `json:"artifacts"`
} `json:"build"`
}
func readManifest(t *testing.T) manifest {
@@ -33,6 +41,23 @@ func readManifest(t *testing.T) manifest {
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
// The module's code and variables are the templates of its items (design 57 L3, issues 795 and 807).
for _, it := range m.Items {
text, err := os.ReadFile(filepath.Join("..", "..", it.Template))
if err != nil {
t.Fatal(err)
}
parts := strings.Split(it.Collection, ":")
switch {
case len(parts) == 3 && parts[0] == "shell":
m.Shell = append(m.Shell, struct{ For, Slot, Code string }{parts[1], parts[2], string(text)})
case it.Collection == "environment" && it.Attributes["kind"] == "variable":
if m.Environment.Variables == nil {
m.Environment.Variables = map[string]string{}
}
m.Environment.Variables[it.Attributes["name"]] = strings.TrimSuffix(string(text), "\n")
}
}
return m
}
@@ -96,9 +121,10 @@ func TestItOwnsTheFilesItsSourcesHoldAndNoQt5Duplicate(t *testing.T) {
pkgs = append(pkgs, r["package"].(string))
continue
}
raw, _ := os.ReadFile(filepath.Join("..", "..", "config", sources[id]))
if r["content"] != string(raw) || r["into"] != nil || r["owner"] != "${machine:account}" {
t.Errorf("%s is not config/%s, whole and the account's", id, sources[id])
_, inline := r["content"]
if _, err := os.Stat(filepath.Join("..", "..", "files", sources[id])); err != nil || inline ||
r["template"] != "files/"+sources[id] || r["into"] != nil || r["owner"] != "${machine:account}" {
t.Errorf("%s is not files/%s, whole, as its template, and the account's", id, sources[id])
}
if strings.Contains(r["path"].(string), "qt5ct") {
t.Error("qt5ct: both workstations run Qt 6 programs through qt6ct; a second tool is a duplicate")
@@ -107,7 +133,7 @@ func TestItOwnsTheFilesItsSourcesHoldAndNoQt5Duplicate(t *testing.T) {
if strings.Join(pkgs, ",") != "gnome-themes-extra,adwaita-icon-theme,adwaita-cursors,qt6ct,xdg-desktop-portal-gtk" {
t.Fatalf("%v", pkgs)
}
qt := readINI(filepath.Join("..", "..", "config", "qt6ct.conf"))
qt := readINI(filepath.Join("..", "..", "files", "qt6ct.conf"))
if qt["Appearance"]["style"] != "Fusion" || qt["Appearance"]["custom_palette"] != "true" || !strings.Contains(qt["Fonts"]["general"], "Inter,11") {
t.Fatalf("%v", qt)
}
@@ -116,7 +142,7 @@ func TestItOwnsTheFilesItsSourcesHoldAndNoQt5Duplicate(t *testing.T) {
t.Fatalf("qt6ct ships no %s", qt["Appearance"]["color_scheme_path"])
}
}
gtk := readINI(filepath.Join("..", "..", "config", "gtk-settings.ini"))["Settings"]
gtk := readINI(filepath.Join("..", "..", "files", "gtk-settings.ini"))["Settings"]
if gtk["gtk-theme-name"] != "Adwaita" || gtk["gtk-application-prefer-dark-theme"] != "1" || gtk["gtk-font-name"] != "Inter 11" {
t.Fatalf("%v", gtk)
}
@@ -244,3 +270,31 @@ func asJSON(v any) string {
b, _ := json.Marshal(v)
return string(b)
}
// No file resource carries its text inline: each names its template in files/, and the build packs files/
// into the one archive the controller reads the templates back from (novox/hq ADR 0349 §2).
func TestEveryFileTakesItsTextFromATemplate(t *testing.T) {
m := readManifest(t)
for _, r := range m.Resources {
if _, inline := r["content"]; inline {
t.Errorf("resource %v keeps its text inline: keep it in files/ and name it as the template", r["id"])
}
if name, ok := r["template"].(string); ok {
if _, err := os.Stat(filepath.Join("..", "..", name)); err != nil {
t.Errorf("resource %v names %s: %v", r["id"], name, err)
}
}
}
packs := 0
for _, a := range m.Build.Artifacts {
if a["kind"] == "archive" && a["from"] == "files" {
packs++
if a["name"] != "files" {
t.Errorf("the templates archive is named %v, not files", a["name"])
}
}
}
if packs != 1 {
t.Errorf("the build packs files/ into %d archives, not one", packs)
}
}
+1
View File
@@ -0,0 +1 @@
Adwaita:dark
+1
View File
@@ -0,0 +1 @@
/usr/share/themes/Adwaita-dark/gtk-2.0/gtkrc
@@ -0,0 +1 @@
qt6ct
+1
View File
@@ -0,0 +1 @@
6
@@ -0,0 +1 @@
Fusion
+1
View File
@@ -0,0 +1 @@
24
+1
View File
@@ -0,0 +1 @@
Adwaita
+10
View File
@@ -0,0 +1,10 @@
# The appearance (module adwaita): GSettings is where the portal reads dark or light, and the portal is
# the only way it reaches Electron, Chromium, Firefox and flatpaks. Set at every session start to the
# module's default; adwaita_appearance switches it for a session.
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' || true
gsettings set org.gnome.desktop.interface gtk-theme 'Adwaita' || true
gsettings set org.gnome.desktop.interface icon-theme 'Adwaita' || true
gsettings set org.gnome.desktop.interface cursor-theme 'Adwaita' || true
gsettings set org.gnome.desktop.interface cursor-size 24 || true
gsettings set org.gnome.desktop.interface font-name 'Inter 11' || true
gsettings set org.gnome.desktop.interface monospace-font-name 'JetBrainsMono Nerd Font 11' || true
+3
View File
@@ -0,0 +1,3 @@
! adwaita: the cursor, for X programs that take it from the resources.
Xcursor.theme: Adwaita
Xcursor.size: 24
+81 -23
View File
@@ -1,6 +1,7 @@
{
"module": "adwaita",
"version": "1",
"moved": true,
"capabilities": [
"package-manager"
],
@@ -10,27 +11,79 @@
"adwaita_icons",
"adwaita_portal_check"
],
"environment": {
"variables": {
"GTK_THEME": "Adwaita:dark",
"GTK2_RC_FILES": "/usr/share/themes/Adwaita-dark/gtk-2.0/gtkrc",
"QT_QPA_PLATFORMTHEME": "qt6ct",
"QT_STYLE_OVERRIDE": "Fusion",
"QT_SELECT": "6",
"XCURSOR_THEME": "Adwaita",
"XCURSOR_SIZE": "24"
}
},
"shell": [
"items": [
{
"for": "xresources",
"slot": "normal",
"code": "! adwaita: the cursor, for X programs that take it from the resources.\nXcursor.theme: Adwaita\nXcursor.size: 24\n"
"collection": "environment",
"name": "gtk-theme",
"attributes": {
"kind": "variable",
"name": "GTK_THEME"
},
"template": "files/env-gtk-theme"
},
{
"for": "xinitrc",
"slot": "normal",
"code": "# The appearance (module adwaita): GSettings is where the portal reads dark or light, and the portal is\n# the only way it reaches Electron, Chromium, Firefox and flatpaks. Set at every session start to the\n# module's default; adwaita_appearance switches it for a session.\ngsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' || true\ngsettings set org.gnome.desktop.interface gtk-theme 'Adwaita' || true\ngsettings set org.gnome.desktop.interface icon-theme 'Adwaita' || true\ngsettings set org.gnome.desktop.interface cursor-theme 'Adwaita' || true\ngsettings set org.gnome.desktop.interface cursor-size 24 || true\ngsettings set org.gnome.desktop.interface font-name 'Inter 11' || true\ngsettings set org.gnome.desktop.interface monospace-font-name 'JetBrainsMono Nerd Font 11' || true\n"
"collection": "environment",
"name": "gtk2-rc-files",
"attributes": {
"kind": "variable",
"name": "GTK2_RC_FILES"
},
"template": "files/env-gtk2-rc-files"
},
{
"collection": "environment",
"name": "qt-qpa-platformtheme",
"attributes": {
"kind": "variable",
"name": "QT_QPA_PLATFORMTHEME"
},
"template": "files/env-qt-qpa-platformtheme"
},
{
"collection": "environment",
"name": "qt-style-override",
"attributes": {
"kind": "variable",
"name": "QT_STYLE_OVERRIDE"
},
"template": "files/env-qt-style-override"
},
{
"collection": "environment",
"name": "qt-select",
"attributes": {
"kind": "variable",
"name": "QT_SELECT"
},
"template": "files/env-qt-select"
},
{
"collection": "environment",
"name": "xcursor-theme",
"attributes": {
"kind": "variable",
"name": "XCURSOR_THEME"
},
"template": "files/env-xcursor-theme"
},
{
"collection": "environment",
"name": "xcursor-size",
"attributes": {
"kind": "variable",
"name": "XCURSOR_SIZE"
},
"template": "files/env-xcursor-size"
},
{
"collection": "shell:xresources:normal",
"name": "xresources-normal",
"template": "files/xresources-normal"
},
{
"collection": "shell:xinitrc:normal",
"name": "xinitrc-normal",
"template": "files/xinitrc-normal"
}
],
"resources": [
@@ -65,7 +118,7 @@
"path": "${machine:account-home}/.config/gtk-3.0/settings.ini",
"owner": "${machine:account}",
"mode": "0644",
"content": "# Written by the mesh (module adwaita, novox/hq ADR 0208), for GTK 3 and GTK 4 alike. Replaced at\n# every push; adwaita_appearance switches dark and light for the running session.\n[Settings]\ngtk-theme-name=Adwaita\ngtk-icon-theme-name=Adwaita\ngtk-cursor-theme-name=Adwaita\ngtk-cursor-theme-size=24\ngtk-font-name=Inter 11\ngtk-application-prefer-dark-theme=1\n"
"template": "files/gtk-settings.ini"
},
{
"id": "gtk4",
@@ -73,7 +126,7 @@
"path": "${machine:account-home}/.config/gtk-4.0/settings.ini",
"owner": "${machine:account}",
"mode": "0644",
"content": "# Written by the mesh (module adwaita, novox/hq ADR 0208), for GTK 3 and GTK 4 alike. Replaced at\n# every push; adwaita_appearance switches dark and light for the running session.\n[Settings]\ngtk-theme-name=Adwaita\ngtk-icon-theme-name=Adwaita\ngtk-cursor-theme-name=Adwaita\ngtk-cursor-theme-size=24\ngtk-font-name=Inter 11\ngtk-application-prefer-dark-theme=1\n"
"template": "files/gtk-settings.ini"
},
{
"id": "qt6ct",
@@ -81,7 +134,7 @@
"path": "${machine:account-home}/.config/qt6ct/qt6ct.conf",
"owner": "${machine:account}",
"mode": "0644",
"content": "[Appearance]\ncolor_scheme_path=/usr/share/qt6ct/colors/darker.conf\ncustom_palette=true\nicon_theme=Adwaita\nstandard_dialogs=default\nstyle=Fusion\n\n[Fonts]\nfixed=\"JetBrainsMono Nerd Font,11,-1,5,50,0,0,0,0,0\"\ngeneral=\"Inter,11,-1,5,50,0,0,0,0,0\"\n\n[Interface]\nactivate_item_on_single_click=1\nbuttonbox_layout=0\ncursor_flash_time=1000\ndialog_buttons_have_icons=1\ndouble_click_interval=400\ngui_effects=@Invalid()\nkeyboard_scheme=2\nmenus_have_icons=true\nshow_shortcuts_in_context_menus=true\nstylesheets=@Invalid()\ntoolbutton_style=4\nunderline_shortcut=1\nwheel_scroll_lines=3\n\n[Troubleshooting]\nforce_raster_widgets=1\nignored_applications=@Invalid()\n"
"template": "files/qt6ct.conf"
},
{
"id": "portals",
@@ -89,7 +142,7 @@
"path": "${machine:account-home}/.config/xdg-desktop-portal/portals.conf",
"owner": "${machine:account}",
"mode": "0644",
"content": "# Written by the mesh (module adwaita, novox/hq ADR 0208). Replaced at every push.\n#\n# Which portal backend answers each interface. i3 is not a desktop xdg-desktop-portal knows, so with\n# no preference it uses whichever backend happens to be installed: fine while gtk is the only one,\n# wrong the day another arrives as somebody else's dependency. Named instead. gtk also serves\n# org.freedesktop.appearance (dark or light) from GSettings, which the session's start sets.\n[preferred]\ndefault=gtk\n# Secrets for sandboxed programs come from the keyring's backend. Without this line no backend answers\n# the interface: gtk does not implement it, and gnome-keyring's names only GNOME as its desktop.\norg.freedesktop.impl.portal.Secret=gnome-keyring\n"
"template": "files/portals.conf"
},
{
"id": "cursor",
@@ -97,7 +150,7 @@
"path": "${machine:account-home}/.icons/default/index.theme",
"owner": "${machine:account}",
"mode": "0644",
"content": "# Written by the mesh (module adwaita, novox/hq ADR 0208): the default cursor theme, for programs that\n# read neither XCURSOR_THEME nor the X resources.\n[Icon Theme]\nName=Default\nInherits=Adwaita\n"
"template": "files/cursor-index.theme"
}
],
"build": {
@@ -112,6 +165,11 @@
"loads": [
"adwaita-tools"
]
},
{
"name": "files",
"kind": "archive",
"from": "files"
}
]
}
+28 -4
View File
@@ -1,7 +1,8 @@
# artifact-store-tools
Tools that say what the mesh's artifact store holds, and what the controller's records say of it
(novox/hq ADR 0251 §1–3, to-be 51). It declares no resources and changes nothing on its machine.
(novox/hq ADR 0251 §1–3, to-be 51). It changes nothing on its machine; its only resources are its state
directory and the file the mesh writes there from its setting `older-store-names`.
## Why a module beside the store
@@ -18,7 +19,7 @@ on that machine.
|---|---|---|
| `artifact_store_repositories` | r | every repository: tags and what each names, how many manifests (tagged or not), size |
| `artifact_store_usage` | r | the store's size, the largest repositories, bytes shared between repositories, bytes no manifest marks (what the nightly collector frees next) |
| `artifact_store_references` | r | what the controller's records say of each manifest, counted and sized by state and repository; each manifest listed when one repository is asked; what the records keep that the store does not hold |
| `artifact_store_references` | r | what the controller's records and the catalogue's definitions say of each manifest, counted and sized by state and repository; each manifest listed when one repository is asked; what the records keep that the store does not hold |
| `artifact_store_collect` | a | a dry run unless `dry_run` is false: what the controller would let go of, and the bytes the nightly collector would free then and now. A real run needs `why` and asks the controller's `collect` |
### How the store is read
@@ -43,6 +44,7 @@ it lists. The store's collector removes every blob no manifest marks, so these n
| state | what | removable |
|---|---|---|
| `kept` | a definition names it, or one of the five most recent builds of a module the mesh holds; `why` says which | no |
| `named-by-a-definition` | no record names it, and a definition in the catalogue pins it by digest, under the store's current name or an older one; `why` names the module (hq issue 323) | no |
| `holder-of-kept-archive` | the manifest that keeps a kept archive's blob (hq issue 253) | no |
| `eligible` | the mesh made it and keeps it for no reason | through the controller's `collect` |
| `holder-of-eligible-archive` | the manifest that keeps an eligible archive's blob | with its archive |
@@ -56,6 +58,20 @@ its own right; the controller lets an index go with the platform manifests only
counted as the index's. Two indexes naming one platform give it the more keeping state. An index no
record names leaves its platform manifests unrecorded with it.
**What the definitions pin** (hq issue 323). A definition may pin an image of the store the mesh did not
make: pushed before the mesh built it, and pinned under the store's current name or a name it had before.
No record of the controller names it, yet a machine runs it, so `artifact_store_references` reads every
definition the catalogue holds (`mesh-catalog.catalog_modules`, then `catalog_module` for each, eight at
a time and within twenty seconds) and finds every `<name>/<repository>@sha256:…` in any of its strings,
under each of the store's names: `artifact-store://`, the store's address as the controller answers it,
and the names it had before. Those are the mesh's setting `older-store-names` (space-separated, `none` by
default, set with the controller's `settings`), which the mesh writes to the module's `store-names` file: a
catalogue names no installation. When they cannot be read, the answer's first line says so. A manifest no record
names that a definition pins is `named-by-a-definition`; one the records name keeps the records' state. A
definition read at its commit is kept, so the next call asks only for the list. Definitions that could
not be read are listed under `definitions.unread`, and the answer's first line says that what they pin
reads as unrecorded until they are.
**Unrecorded copies of upstream images** are listed in `unrecorded_mirrors`: each manifest at the top of
a repository only the build's mirror writes (`upstream/<host>/<path>`, or a module's `<module>/on-<argument>`
from before hq ADR 0257) that no record names, with what it and its platforms mark alone. They are the
@@ -88,6 +104,14 @@ with slashes, a cut listing refused), manifests read with one absent or unreadab
digest never becoming a path outside the blobs, the mark (an index, a shared blob, a blob nothing marks),
every state, what the records keep that the store lacks, bytes freed counting a blob shared with a kept
manifest as kept, a dry run never confirming, a real run without `why` refused before anything is asked,
the fall-back when the references let go of cannot be carried, escalation through `sudo -n`, and that the
tools served are exactly the manifest's `tools` and its `invokes` exactly the two controller verbs.
the fall-back when the references let go of cannot be carried, a manifest a definition pins under the
store's current or an older name, a definition not read said first, escalation through `sudo -n`, and that the
tools served are exactly the manifest's `tools` and its `invokes` exactly the two controller verbs and the
catalogue's two tools.
Any command other than the two read-only scripts fails the test.
## Files it does not know
Each directory or archive this module declares says how it treats a file it did not write (ADR 0422, ADR 0416).
- `${dir:state}`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
@@ -7,12 +7,15 @@ import (
// The states a manifest the store holds can be in, as the records see it (novox/hq to-be 51).
const (
StateKept = "kept"
StateHolderKept = "holder-of-kept-archive"
StateEligible = "eligible"
StateHolderEligible = "holder-of-eligible-archive"
StateCollectedPresent = "let-go-yet-present"
StateNamedDocument = "named-document"
StateKept = "kept"
StateHolderKept = "holder-of-kept-archive"
StateEligible = "eligible"
StateHolderEligible = "holder-of-eligible-archive"
StateCollectedPresent = "let-go-yet-present"
StateNamedDocument = "named-document"
// StateNamedByDefinition is a manifest no record names that a definition pins by digest, under the
// store's current name or an older one (novox/hq issue 323): a machine may run it, so it is kept.
StateNamedByDefinition = "named-by-a-definition"
StateUnrecorded = "unrecorded"
collectionRemovesThese = "eligible and holder-of-eligible-archive, through the controller's collect; nothing else"
)
@@ -45,8 +48,8 @@ var mirrorRepository = regexp.MustCompile(`^(upstream/[a-z0-9][a-z0-9._/-]*|[a-z
// keeping orders the states a platform manifest may take from the indexes naming it: the most keeping
// wins, so a platform two indexes name is as kept as the more kept of them.
var keeping = map[string]int{
StateKept: 0, StateNamedDocument: 1, StateHolderKept: 2, StateEligible: 3, StateHolderEligible: 4,
StateCollectedPresent: 5,
StateKept: 0, StateNamedByDefinition: 1, StateNamedDocument: 2, StateHolderKept: 3, StateEligible: 4,
StateHolderEligible: 5, StateCollectedPresent: 6,
}
// recordIndex finds a record by repository and digest.
@@ -64,8 +67,9 @@ func indexRecords(r *Records) recordIndex {
return idx
}
// Classify gives every manifest the store holds one state.
func Classify(v *View, recs *Records) []Entry {
// Classify gives every manifest the store holds one state. A manifest no record names that a definition
// pins is named by a definition; defs may be nil, when the definitions were not read.
func Classify(v *View, recs *Records, defs *Definitions) []Entry {
idx := indexRecords(recs)
var out []Entry
for _, name := range v.L.RepoNames() {
@@ -99,6 +103,12 @@ func Classify(v *View, recs *Records) []Entry {
e.State = StateNamedDocument
}
}
if e.State == StateUnrecorded && defs != nil {
for _, p := range defs.Pins[Key(name, d)] {
e.State = StateNamedByDefinition
e.Why = append(e.Why, p.why())
}
}
out = append(out, e)
}
}
@@ -0,0 +1,343 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"regexp"
"sort"
"strings"
"sync"
"time"
)
// What the definitions pin in the store (novox/hq issue 323). A definition may pin an image of the store by
// digest that the mesh did not make: one pushed before the mesh built it, pinned under the store's current
// name or under a name the store had before. No record of the controller names such a manifest, and yet a
// machine runs it, so it is said to be named by a definition and never unrecorded.
// NamesFileEnv names the file the mesh writes from the setting older-store-names: a line `older=` with the
// names the store had before, as a definition may still write them — host names, or host:port, separated by
// spaces — or `none`. A catalogue names no installation, so the names are the mesh's setting, not the
// manifest's.
const NamesFileEnv = "MESH_ARTIFACT_STORE_NAMES_FILE"
// CatalogueModules and CatalogueModule are the catalogue's tools these definitions are read through.
const (
CatalogueModules = "mesh-catalog.catalog_modules"
CatalogueModule = "mesh-catalog.catalog_module"
)
// DefinitionsTimeout bounds reading every definition, below the runtime's thirty-second call limit; what
// was read is kept, so a call that runs out reads only the rest the next time.
const DefinitionsTimeout = 20 * time.Second
// definitionReaders is how many definitions are asked at once.
const definitionReaders = 8
// StoreNames are the names a definition may pin an image of the store under: the scheme the mesh writes
// (artifact-store://), the store's address now, and the names it had before.
type StoreNames struct {
Current []string
Older []string
}
// NamesOf is the store's names: its address as the controller answers it, and the older ones given.
func NamesOf(address, older string) StoreNames {
n := StoreNames{Current: []string{"artifact-store://"}}
if a := strings.Trim(strings.TrimSpace(address), "/"); a != "" {
n.Current = append(n.Current, a+"/")
}
for _, o := range strings.FieldsFunc(older, func(r rune) bool { return r == ',' || r == ' ' || r == '\n' || r == '\t' }) {
if o = strings.Trim(o, "/"); o != "" && o != "none" {
n.Older = append(n.Older, o+"/")
}
}
return n
}
// OlderNames reads the older names from the file the mesh writes, and says why when it cannot.
func OlderNames() (string, string) {
path := os.Getenv(NamesFileEnv)
if path == "" {
return "", NamesFileEnv + " is not set, so no older name of the store was read"
}
raw, err := os.ReadFile(path)
if err != nil {
return "", "the store's older names could not be read: " + err.Error()
}
for _, line := range strings.Split(string(raw), "\n") {
if v, ok := strings.CutPrefix(strings.TrimSpace(line), "older="); ok {
return v, ""
}
}
return "", path + " holds no line older=, so no older name of the store was read"
}
// Pin is one definition pinning a manifest of the store by digest.
type Pin struct {
Module string `json:"module"`
// Name is the store's name it is pinned under, as written: "artifact-store://", an address, or an older name.
Name string `json:"name"`
Older bool `json:"older,omitempty"`
}
// pinned is one pin found in a manifest: where in the store, and under which name.
type pinned struct {
Repository, Digest, Name string
Older bool
}
var pinShapes sync.Map // name -> *regexp.Regexp
// pinShape is `<name><repository>@sha256:<hex>`, the name standing on its own: not the tail of a longer host.
func pinShape(name string) *regexp.Regexp {
if re, ok := pinShapes.Load(name); ok {
return re.(*regexp.Regexp)
}
re := regexp.MustCompile(`(?:^|[^A-Za-z0-9.-])` + regexp.QuoteMeta(name) +
`([a-z0-9][a-z0-9._/-]*)@(sha256:[0-9a-f]{64})`)
pinShapes.Store(name, re)
return re
}
// PinsIn finds every image of the store a manifest pins by digest, in any string it holds: a resource's
// image, a build's base, a file's content.
func PinsIn(manifest json.RawMessage, names StoreNames) []pinned {
var doc any
if json.Unmarshal(manifest, &doc) != nil {
return nil
}
seen := map[string]bool{}
var out []pinned
var walk func(any)
walk = func(x any) {
switch v := x.(type) {
case map[string]any:
for _, e := range v {
walk(e)
}
case []any:
for _, e := range v {
walk(e)
}
case string:
for _, group := range []struct {
names []string
older bool
}{{names.Current, false}, {names.Older, true}} {
for _, name := range group.names {
for _, m := range pinShape(name).FindAllStringSubmatch(v, -1) {
p := pinned{Repository: strings.Trim(m[1], "/"), Digest: m[2], Name: name, Older: group.older}
if k := p.Repository + "@" + p.Digest + " " + p.Name; !seen[k] {
seen[k] = true
out = append(out, p)
}
}
}
}
}
}
walk(doc)
return out
}
// Definitions is what the catalogue's definitions pin in the store, and what of them could not be read.
type Definitions struct {
// Pins are by Key(repository, digest).
Pins map[string][]Pin
// Read is how many definitions were read; Unread says, per module, why its definition was not.
Read int
Unread map[string]string
// NotAsked says why the catalogue could not be asked at all, when it could not.
NotAsked string
// NamesNotRead says why the store's older names could not be read, when they could not.
NamesNotRead string
Names StoreNames
}
// Catalogue reads the definitions through the catalogue's tools, and keeps each definition it read by its
// module and commit, which never change together.
type Catalogue struct {
Ask Asking
mu sync.Mutex
kept map[string][]pinned // "<module>@<commit>|<names>" -> its pins
}
type catalogueEntry struct {
Module string `json:"module"`
Commit string `json:"commit"`
}
// Read asks the catalogue for every module and reads each definition, bounded by DefinitionsTimeout.
func (c *Catalogue) Read(ctx context.Context, names StoreNames) *Definitions {
defs := &Definitions{Pins: map[string][]Pin{}, Unread: map[string]string{}, Names: names}
if c == nil || c.Ask == nil {
defs.NotAsked = "this bundle has no way to ask the catalogue"
return defs
}
raw, err := c.Ask(CatalogueModules, map[string]any{})
if err != nil {
defs.NotAsked = "the catalogue's modules could not be asked: " + err.Error()
return defs
}
answer, output, ok := answerOf(raw)
if !ok {
defs.NotAsked = "the catalogue refused its modules: " + lastLine(output)
return defs
}
var list struct {
Modules []catalogueEntry `json:"modules"`
Error string `json:"error"`
}
if err := json.Unmarshal(answer, &list); err != nil || list.Error != "" || list.Modules == nil {
defs.NotAsked = "the catalogue's modules answer is not readable: " + firstOf(list.Error, errText(err), lastLine(output))
return defs
}
c.mu.Lock()
if c.kept == nil {
c.kept = map[string][]pinned{}
}
c.mu.Unlock()
nameKey := strings.Join(names.Current, ",") + ";" + strings.Join(names.Older, ",")
ctx, cancel := context.WithTimeout(ctx, DefinitionsTimeout)
defer cancel()
type result struct {
module string
pins []pinned
why string
}
results := make(chan result, len(list.Modules))
work := make(chan catalogueEntry)
var wg sync.WaitGroup
for i := 0; i < definitionReaders; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for m := range work {
pins, why := c.definition(m, names, nameKey)
results <- result{m.Module, pins, why}
}
}()
}
go func() {
defer close(work)
for _, m := range list.Modules {
select {
case work <- m:
case <-ctx.Done():
return
}
}
}()
go func() { wg.Wait(); close(results) }()
got := map[string]bool{}
collect:
for {
select {
case r, open := <-results:
if !open {
break collect
}
got[r.module] = true
if r.why != "" {
defs.Unread[r.module] = r.why
continue
}
defs.Read++
for _, p := range r.pins {
k := Key(p.Repository, p.Digest)
defs.Pins[k] = append(defs.Pins[k], Pin{Module: r.module, Name: p.Name, Older: p.Older})
}
case <-ctx.Done():
break collect
}
}
for _, m := range list.Modules {
if !got[m.Module] {
defs.Unread[m.Module] = fmt.Sprintf("not read within %d s", int(DefinitionsTimeout/time.Second))
}
}
for k := range defs.Pins {
sort.Slice(defs.Pins[k], func(i, j int) bool { return defs.Pins[k][i].Module < defs.Pins[k][j].Module })
}
return defs
}
// definition reads one module's definition, or answers it from what was read before.
func (c *Catalogue) definition(m catalogueEntry, names StoreNames, nameKey string) ([]pinned, string) {
key := m.Module + "@" + m.Commit + "|" + nameKey
if m.Commit != "" {
c.mu.Lock()
pins, had := c.kept[key]
c.mu.Unlock()
if had {
return pins, ""
}
}
args := map[string]any{"module": m.Module}
if m.Commit != "" {
args["commit"] = m.Commit
}
raw, err := c.Ask(CatalogueModule, args)
if err != nil {
return nil, "could not be asked: " + err.Error()
}
answer, output, ok := answerOf(raw)
if !ok {
return nil, "refused: " + lastLine(output)
}
var one struct {
Module *struct {
Manifest json.RawMessage `json:"manifest"`
} `json:"module"`
Error string `json:"error"`
}
if err := json.Unmarshal(answer, &one); err != nil || one.Error != "" || one.Module == nil || len(one.Module.Manifest) == 0 {
return nil, "not readable: " + firstOf(one.Error, errText(err), "no manifest in the answer")
}
pins := PinsIn(one.Module.Manifest, names)
if m.Commit != "" {
c.mu.Lock()
c.kept[key] = pins
c.mu.Unlock()
}
return pins, ""
}
// why says which definitions name a manifest, and under which of the store's names.
func (p Pin) why() string {
if p.Older {
return fmt.Sprintf("a definition names it: %s, under %s, an older name of the store", p.Module, shownName(p.Name))
}
return "a definition names it: " + p.Module
}
func firstOf(s ...string) string {
for _, x := range s {
if strings.TrimSpace(x) != "" {
return x
}
}
return ""
}
func errText(err error) string {
if err == nil {
return ""
}
return err.Error()
}
// shownName is a name of the store as a person reads it: a host without the slash a pin puts after it, the
// scheme as written.
func shownName(name string) string {
if strings.HasSuffix(name, "://") {
return name
}
return strings.TrimSuffix(name, "/")
}
@@ -0,0 +1,259 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
// A definition may pin an image of the store the mesh did not make, under a name the store had before
// (novox/hq issue 323): three apps the anchor runs are pinned so. No record names them; a definition does.
const oldName = "registry-api.example.org"
// container is a definition whose one container runs image.
func container(image string) string {
return fmt.Sprintf(`{"module":"x","resources":[{"id":"server","type":"container","image":%q}]}`, image)
}
func TestAnImageADefinitionPinsUnderAnOlderNameOfTheStoreIsNamedByADefinition(t *testing.T) {
v := view(t, world())
names := NamesOf("store:5000", oldName)
defs := &Definitions{Pins: map[string][]Pin{}}
for module, manifest := range map[string]string{
// m-stranger: no record names it; the invoicing app pins it under the store's older name.
"invoicing": container(oldName + "/app/server@" + d("m-stranger")),
// m-old: the records say eligible; a definition pinning it does not change what the records say.
"website": container("store:5000/app/server@" + d("m-old")),
} {
for _, p := range PinsIn(json.RawMessage(manifest), names) {
defs.Pins[Key(p.Repository, p.Digest)] = append(defs.Pins[Key(p.Repository, p.Digest)], Pin{Module: module, Name: p.Name, Older: p.Older})
}
}
got := stateByName(Classify(v, withFill(records()), defs))
stranger := got["app/server "+d("m-stranger")]
if stranger.State != StateNamedByDefinition {
t.Fatalf("a manifest a definition pins under an older name reads %q, want %q", stranger.State, StateNamedByDefinition)
}
if want := []string{"a definition names it: invoicing, under " + oldName + ", an older name of the store"}; !reflect.DeepEqual(stranger.Why, want) {
t.Errorf("why %q, want %q", stranger.Why, want)
}
if s := got["app/server "+d("m-old")].State; s != StateEligible {
t.Errorf("a recorded manifest a definition pins reads %q: the records decide what the mesh made", s)
}
// Without the definitions it is what it was: unrecorded.
if s := stateByName(Classify(v, withFill(records()), nil))["app/server "+d("m-stranger")].State; s != StateUnrecorded {
t.Errorf("without definitions %q", s)
}
}
func TestPinsAreFoundInEveryStringAndOnlyUnderTheStoresNames(t *testing.T) {
names := NamesOf("store:5000", oldName+", older.example/")
manifest := `{"resources":[
{"type":"container","image":"` + oldName + `/team/www@` + d("www") + `"},
{"type":"file","content":"IMAGE=older.example/team/tool@` + d("tool") + `\n"},
{"type":"container","image":"artifact-store://mod/art@` + d("built") + `"},
{"type":"container","image":"store:5000/mod/art@` + d("addressed") + `"},
{"type":"container","image":"not` + oldName + `/team/www@` + d("lookalike") + `"},
{"type":"container","image":"docker.io/library/nginx@` + d("upstream") + `"},
{"type":"container","image":"` + oldName + `/team/www:latest"}],
"build":{"on":{"base":"` + oldName + `/team/www@` + d("www") + `"}}}`
var got []string
for _, p := range PinsIn(json.RawMessage(manifest), names) {
got = append(got, fmt.Sprintf("%s@%s %s %v", p.Repository, p.Digest, p.Name, p.Older))
}
want := map[string]bool{
"team/www@" + d("www") + " " + oldName + "/ true": true,
"team/tool@" + d("tool") + " older.example/ true": true,
"mod/art@" + d("built") + " artifact-store:// false": true,
"mod/art@" + d("addressed") + " store:5000/ false": true,
}
if len(got) != len(want) {
t.Fatalf("found %d pins, want %d (one per image and name; not a longer host's tail, not another registry, not a tag): %v", len(got), len(want), got)
}
for _, g := range got {
if !want[g] {
t.Errorf("found %s", g)
}
}
}
func TestAPlatformTakesTheStateOfAnIndexADefinitionNames(t *testing.T) {
v := mirrorWorld()
defs := &Definitions{Pins: map[string][]Pin{
Key("route-proxy/on-go_base", d("i-pre")): {{Module: "route-proxy", Name: oldName + "/", Older: true}},
}}
got := stateByName(Classify(v, withFill(mirrorRecords()), defs))
for _, name := range []string{"i-pre", "p-pre"} {
if s := got["route-proxy/on-go_base "+d(name)].State; s != StateNamedByDefinition {
t.Errorf("%s reads %q, want %q", name, s, StateNamedByDefinition)
}
}
if e := got["route-proxy/on-go_base "+d("p-pre")]; e.Index != d("i-pre") {
t.Errorf("p-pre takes its state from %q", e.Index)
}
if s := got["route-proxy/on-go_base "+d("p-lost")].State; s != StateUnrecorded {
t.Errorf("a platform nothing names reads %q", s)
}
for _, um := range UnrecordedMirrors(v, Classify(v, withFill(mirrorRecords()), defs)) {
if strings.Contains(um.Reference, hexOf(d("i-pre"))) {
t.Errorf("a copy a definition names is offered to the mirrors verb: %v", um)
}
}
}
// olderNames writes the file the mesh writes from the setting older-store-names.
func olderNames(t *testing.T, names string) {
t.Helper()
path := filepath.Join(t.TempDir(), "store-names")
if err := os.WriteFile(path, []byte("# written by the mesh\nolder="+names+"\n"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv(NamesFileEnv, path)
}
func TestTheOlderNamesAreReadFromTheFileTheMeshWrites(t *testing.T) {
olderNames(t, oldName+" older.example")
got, why := OlderNames()
if why != "" || !reflect.DeepEqual(NamesOf("", got).Older, []string{oldName + "/", "older.example/"}) {
t.Errorf("read %q (%s)", got, why)
}
olderNames(t, "none")
if got, why := OlderNames(); why != "" || len(NamesOf("", got).Older) != 0 {
t.Errorf("none read as %q (%s)", got, why)
}
t.Setenv(NamesFileEnv, filepath.Join(t.TempDir(), "absent"))
if _, why := OlderNames(); why == "" {
t.Error("an absent file was not said")
}
}
func TestOlderNamesNotReadAreSaidFirst(t *testing.T) {
t.Setenv(NamesFileEnv, "")
a := &asked{definitions: map[string]string{"invoicing": container(oldName + "/app/server@" + d("m-stranger"))}}
out, err := references(t, a)(map[string]any{"repository": "app/server"})
if err != nil {
t.Fatal(err)
}
m := out.(map[string]any)
if w := m["said"].([]string)[0]; !strings.Contains(w, NamesFileEnv) {
t.Errorf("first said %q", w)
}
if s := manifestsOf(t, out)["app/server "+d("m-stranger")].State; s != StateUnrecorded {
t.Errorf("m-stranger reads %q without the older names", s)
}
}
// references is one artifact_store_references tool, kept across calls as the runtime keeps the bundle.
func references(t *testing.T, a *asked) func(map[string]any) (any, error) {
f := world()
s := &Store{Container: "mesh-registry", Run: f.run(t)}
for _, tl := range Tools(s, Controller{Ask: a.ask}, &Catalogue{Ask: a.ask}) {
if tl.Name == "artifact_store_references" {
return tl.Run
}
}
t.Fatal("no artifact_store_references")
return nil
}
func manifestsOf(t *testing.T, out any) map[string]Entry {
t.Helper()
entries, ok := out.(map[string]any)["manifests"].([]Entry)
if !ok {
t.Fatalf("no manifests in %v", out)
}
return stateByName(entries)
}
func TestReferencesReadTheDefinitionsUnderTheStoresOlderNames(t *testing.T) {
olderNames(t, oldName)
a := &asked{definitions: map[string]string{
"invoicing": container(oldName + "/app/server@" + d("m-stranger")),
"grafana": container("grafana/grafana@" + d("upstream")),
}}
run := references(t, a)
out, err := run(map[string]any{"repository": "app/server"})
if err != nil {
t.Fatal(err)
}
e := manifestsOf(t, out)["app/server "+d("m-stranger")]
if e.State != StateNamedByDefinition || len(e.Why) != 1 || !strings.Contains(e.Why[0], "invoicing") || !strings.Contains(e.Why[0], oldName) {
t.Errorf("m-stranger: %+v", e)
}
said := out.(map[string]any)["definitions"].(map[string]any)
if said["read"] != 2 || !reflect.DeepEqual(said["older_store_names"], []string{oldName}) ||
!reflect.DeepEqual(said["store_names"], []string{"artifact-store://", "store:5000"}) {
t.Errorf("definitions said %v", said)
}
if w := out.(map[string]any)["said"].([]string)[0]; strings.Contains(w, "could not be read") {
t.Errorf("a warning with every definition read: %s", w)
}
// A definition read at its commit is kept: the next call asks only for the list.
before := 0
for _, c := range a.calls {
if c == CatalogueModule {
before++
}
}
if _, err := run(map[string]any{"repository": "app/server"}); err != nil {
t.Fatal(err)
}
after := 0
for _, c := range a.calls {
if c == CatalogueModule {
after++
}
}
if before != 2 || after != 2 {
t.Errorf("asked catalog_module %d times, then %d: each definition at its commit is read once", before, after)
}
}
func TestADefinitionNotReadIsSaidFirstAndLeavesWhatItPinsUnrecorded(t *testing.T) {
olderNames(t, oldName)
a := &asked{
definitions: map[string]string{"invoicing": container(oldName + "/app/server@" + d("m-stranger")), "grafana": container("x")},
failing: map[string]bool{"invoicing": true},
}
out, err := references(t, a)(map[string]any{"repository": "app/server"})
if err != nil {
t.Fatal(err)
}
m := out.(map[string]any)
if s := manifestsOf(t, out)["app/server "+d("m-stranger")].State; s != StateUnrecorded {
t.Errorf("m-stranger reads %q with its definition unread", s)
}
if w := m["said"].([]string)[0]; !strings.Contains(w, "1 definitions could not be read") {
t.Errorf("first said %q", w)
}
if u := m["definitions"].(map[string]any)["unread"].(map[string]string); u["invoicing"] == "" || len(u) != 1 {
t.Errorf("unread %v", u)
}
// The catalogue not answering at all: the records are still answered, and the warning stands first.
a = &asked{noCatalogue: true}
out, err = references(t, a)(map[string]any{})
if err != nil {
t.Fatal(err)
}
m = out.(map[string]any)
if w := m["said"].([]string)[0]; !strings.Contains(w, "the definitions could not be read") || !strings.Contains(w, "no responders") {
t.Errorf("first said %q", w)
}
if m["definitions"].(map[string]any)["not_asked"] == nil {
t.Errorf("definitions %v", m["definitions"])
}
}
func TestNoCatalogueIsSaidAndNothingIsAsked(t *testing.T) {
defs := (&Catalogue{}).Read(context.Background(), NamesOf("", ""))
if defs.NotAsked == "" || defs.Read != 0 {
t.Errorf("%+v", defs)
}
}
@@ -24,7 +24,7 @@ func main() {
UID: os.Getuid(),
}
// An empty name serves as the module the runtime names (MESH_SERVED_MODULE): artifact-store-tools.
if err := stdio.Serve("", Tools(s, Controller{Ask: stdio.Ask})); err != nil {
if err := stdio.Serve("", Tools(s, Controller{Ask: stdio.Ask}, &Catalogue{Ask: stdio.Ask})); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
@@ -65,7 +65,7 @@ func TestAPlatformManifestTakesTheStateOfTheIndexNamingIt(t *testing.T) {
for i := range recs.References {
fill(&recs.References[i])
}
got := stateByName(Classify(v, recs))
got := stateByName(Classify(v, recs, nil))
up := "upstream/docker.io/library/golang "
for name, want := range map[string]string{
up + d("p-amd"): StateEligible, // only the eligible index names it
@@ -92,7 +92,7 @@ func TestAPlatformManifestTakesTheStateOfTheIndexNamingIt(t *testing.T) {
if len(mirrors) != 2 || !got["route-proxy/on-go_base "+d("i-pre")].Mirror || !got["route-proxy/on-go_base "+d("p-lost")].Mirror {
t.Errorf("offered as unrecorded copies: %v", mirrors)
}
um := UnrecordedMirrors(v, Classify(v, recs))
um := UnrecordedMirrors(v, Classify(v, recs, nil))
if len(um) != 2 || um[0].Reference != "artifact-store://route-proxy/on-go_base@"+d("i-pre") || um[0].OnlyTheir != 1+1+1000 {
t.Errorf("unrecorded copies: %+v", um)
}
@@ -104,7 +104,7 @@ func TestLettingAnIndexGoTakesItsOwnPlatformsAndSaysWhatStaysAndNamesOne(t *test
for i := range recs.References {
fill(&recs.References[i])
}
entries := Classify(v, recs)
entries := Classify(v, recs, nil)
old := "artifact-store://upstream/docker.io/library/golang@" + d("i-old")
gone := LetGoKeys(v, []string{old}, Spared(v, entries))
up := "upstream/docker.io/library/golang"
@@ -215,7 +215,7 @@ func TestTheMarkIsTheCollectorsIncludingAnIndexAndSharedBlobs(t *testing.T) {
func TestEveryStateIsGiven(t *testing.T) {
v := view(t, world())
got := map[string]string{}
for _, e := range Classify(v, withFill(records())) {
for _, e := range Classify(v, withFill(records()), nil) {
got[e.Repository+" "+e.Digest] = e.State
}
want := map[string]string{
@@ -239,7 +239,7 @@ func TestALetGoReferenceStillPresentIsSaidSo(t *testing.T) {
v := view(t, world())
r := records()
r.References = append(r.References, Record{Reference: "artifact-store://app/server@" + d("m-stranger"), State: "collected"})
for _, e := range Classify(v, withFill(r)) {
for _, e := range Classify(v, withFill(r), nil) {
if e.Digest == d("m-stranger") && e.State != StateCollectedPresent {
t.Errorf("state %s", e.State)
}
@@ -270,7 +270,7 @@ func TestBytesFreedCountASharedBlobAsKept(t *testing.T) {
func TestOnlyTheirBytesLeaveOutWhatAnotherStateMarks(t *testing.T) {
v := view(t, world())
entries := Classify(v, withFill(records()))
entries := Classify(v, withFill(records()), nil)
sums := Summarise(v, entries, entries)
el := sums[StateEligible]
if el.OnlyTheirBytes != 500+v.L.Blobs[d("m-old")] {
@@ -284,6 +284,11 @@ type asked struct {
calls []string
args []map[string]any
fail map[string]bool
// definitions are the catalogue's modules and their manifests; failing, the modules whose
// definition cannot be read; noCatalogue, the catalogue not answering at all.
definitions map[string]string
failing map[string]bool
noCatalogue bool
}
func (a *asked) ask(key string, body any) (json.RawMessage, error) {
@@ -299,6 +304,31 @@ func (a *asked) ask(key string, body any) (json.RawMessage, error) {
switch key {
case "seat:mesh-controller.artifacts":
answer = records()
case CatalogueModules:
if a.noCatalogue {
return nil, fmt.Errorf("no responders")
}
ms := []map[string]any{}
for name := range a.definitions {
ms = append(ms, map[string]any{"module": name, "commit": "c-" + name, "path": "modules/" + name})
}
answer = map[string]any{"modules": ms}
case CatalogueModule:
name, _ := args["module"].(string)
if a.failing[name] {
return nil, fmt.Errorf("no answer")
}
if args["commit"] != "c-"+name {
return nil, fmt.Errorf("asked %s at %v", name, args["commit"])
}
manifest, has := a.definitions[name]
if !has {
answer = map[string]any{"error": "the catalogue holds no " + name}
break
}
// The catalogue's tool answers through the runtime as the protocol's content list.
raw, _ := json.Marshal(map[string]any{"module": map[string]any{"module": name, "manifest": json.RawMessage(manifest)}})
return json.Marshal(map[string]any{"content": []map[string]any{{"type": "text", "text": string(raw)}}})
case "seat:mesh-controller.collect":
if args["confirm"] == "true" {
answer = CollectAnswer{LetGo: []string{"artifact-store://app/server@" + d("m-old")}}
@@ -315,7 +345,7 @@ func (a *asked) ask(key string, body any) (json.RawMessage, error) {
func tool(t *testing.T, a *asked, name string) func(map[string]any) (any, error) {
f := world()
s := &Store{Container: "mesh-registry", Run: f.run(t)}
for _, tl := range Tools(s, Controller{Ask: a.ask}) {
for _, tl := range Tools(s, Controller{Ask: a.ask}, &Catalogue{Ask: a.ask}) {
if tl.Name == name {
return tl.Run
}
@@ -386,7 +416,7 @@ func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
t.Fatal(err)
}
var served []string
for _, tl := range Tools(&Store{}, Controller{}) {
for _, tl := range Tools(&Store{}, Controller{}, &Catalogue{}) {
if !strings.HasPrefix(tl.Name, "artifact_store_") || tl.Description == "" || tl.Run == nil {
t.Errorf("tool %q", tl.Name)
}
@@ -398,7 +428,7 @@ func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
if !reflect.DeepEqual(served, listed) {
t.Fatalf("served %v, manifest %v", served, listed)
}
want := []string{"seat:mesh-controller.artifacts", "seat:mesh-controller.collect"}
want := []string{"seat:mesh-controller.artifacts", "seat:mesh-controller.collect", CatalogueModules, CatalogueModule}
if !reflect.DeepEqual(m.Invokes, want) {
t.Errorf("invokes %v", m.Invokes)
}
@@ -0,0 +1,64 @@
package main
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
// The module's directory, where its manifest and its files/ are.
const moduleDir = "../.."
// templateText is the text a file resource places on the machine: its template's, in the module's files/
// (novox/hq ADR 0349 §2). A resource that names no template, or a template that is not there, fails the test.
func templateText(t *testing.T, r map[string]any) string {
t.Helper()
name, _ := r["template"].(string)
if name == "" {
t.Fatalf("resource %v names no template", r["id"])
}
raw, err := os.ReadFile(filepath.Join(moduleDir, filepath.FromSlash(name)))
if err != nil {
t.Fatalf("resource %v names %s: %v", r["id"], name, err)
}
return string(raw)
}
// No file resource carries its text inline: each names its template in files/, and the build packs files/
// into the one archive the controller reads the templates back from (novox/hq ADR 0349 §2).
func TestEveryFileTakesItsTextFromATemplate(t *testing.T) {
raw, err := os.ReadFile(filepath.Join(moduleDir, "module.json"))
if err != nil {
t.Fatal(err)
}
var m struct {
Resources []map[string]any `json:"resources"`
Build struct {
Artifacts []map[string]any `json:"artifacts"`
} `json:"build"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
for _, r := range m.Resources {
if _, inline := r["content"]; inline {
t.Errorf("resource %v keeps its text inline: keep it in files/ and name it as the template", r["id"])
}
if r["type"] == "file" {
templateText(t, r)
}
}
packs := 0
for _, a := range m.Build.Artifacts {
if a["kind"] == "archive" && a["from"] == "files" {
packs++
if a["name"] != "files" {
t.Errorf("the templates archive is named %v, not files", a["name"])
}
}
}
if packs != 1 {
t.Errorf("the build packs files/ into %d archives, not one", packs)
}
}
@@ -13,7 +13,7 @@ import (
// Tools are the artifact store's tools (novox/hq ADR 0251 §1). The first three only read; artifact_store_collect
// only reads too, unless it is a real run, and then it asks the controller, which decides and deletes.
func Tools(s *Store, c Controller) []stdio.Tool {
func Tools(s *Store, c Controller, cat *Catalogue) []stdio.Tool {
ctx := context.Background()
repoArg := map[string]any{"type": "string", "description": "one repository, as the store names it (<module>/<artifact>)"}
return []stdio.Tool{
@@ -53,7 +53,9 @@ func Tools(s *Store, c Controller) []stdio.Tool {
{
Name: "artifact_store_references",
Description: "What the controller's records say of each manifest the artifact store holds: kept (a definition " +
"names it, or one of the five most recent builds of a module the mesh holds), the holder of a kept archive, " +
"names it, or one of the five most recent builds of a module the mesh holds), named by a definition (no " +
"record names it, and a definition in the catalogue pins it by digest, under the store's current name or " +
"an older one: the module is said), the holder of a kept archive, " +
"eligible (the mesh made it and keeps it for no reason), the holder of an eligible archive, let go yet present, " +
"a named document the controller keeps under a tag, or unrecorded — no record names it. A platform manifest " +
"takes the state of the recorded index naming it. Counted and sized by " +
@@ -67,11 +69,19 @@ func Tools(s *Store, c Controller) []stdio.Tool {
if err != nil {
return nil, err
}
// The definitions are read while the store is: each bounded below the runtime's call limit.
read := make(chan *Definitions, 1)
go func() {
older, why := OlderNames()
defs := cat.Read(ctx, NamesOf(recs.Store, older))
defs.NamesNotRead = why
read <- defs
}()
v, err := s.View(ctx)
if err != nil {
return nil, err
}
return References(v, recs, repo)
return References(v, recs, <-read, repo)
},
},
{
@@ -116,7 +126,7 @@ func Tools(s *Store, c Controller) []stdio.Tool {
if err != nil {
return map[string]any{"controller": answer, "bytes_not_worked_out": "the records could not be read: " + err.Error()}, nil
}
return Collected(v, answer, dry, Classify(v, recs)), nil
return Collected(v, answer, dry, Classify(v, recs, nil)), nil
},
},
}
@@ -225,11 +235,11 @@ func Usage(v *View, top int) map[string]any {
}
// References answers artifact_store_references.
func References(v *View, recs *Records, only string) (map[string]any, error) {
func References(v *View, recs *Records, defs *Definitions, only string) (map[string]any, error) {
if only != "" && v.L.Repos[only] == nil {
return nil, fmt.Errorf("the store holds no repository %q", only)
}
entries := Classify(v, recs)
entries := Classify(v, recs, defs)
byRepo := map[string][]Entry{}
for _, e := range entries {
byRepo[e.Repository] = append(byRepo[e.Repository], e)
@@ -243,7 +253,7 @@ func References(v *View, recs *Records, only string) (map[string]any, error) {
}
whole := Summarise(v, entries, entries)
said := []string{}
for _, state := range []string{StateKept, StateHolderKept, StateEligible, StateHolderEligible, StateCollectedPresent, StateNamedDocument, StateUnrecorded} {
for _, state := range []string{StateKept, StateNamedByDefinition, StateHolderKept, StateEligible, StateHolderEligible, StateCollectedPresent, StateNamedDocument, StateUnrecorded} {
if s := whole[state]; s != nil {
said = append(said, fmt.Sprintf("%s: %d manifests marking %s, %s of it marked by nothing else", state, s.Manifests, human(s.Bytes), human(s.OnlyTheirBytes)))
}
@@ -276,9 +286,57 @@ func References(v *View, recs *Records, only string) (map[string]any, error) {
if u := unreadNote(v); u != nil {
out["unread"] = u
}
out["definitions"] = definitionsSaid(defs)
if warn := definitionsWarning(defs); warn != "" {
// Said first: what it leaves out is a manifest a machine may run, read as unrecorded.
out["said"] = append([]string{warn}, said...)
}
return out, nil
}
// definitionsSaid is how the definitions were read: how many, under which names, and what was not read.
func definitionsSaid(defs *Definitions) map[string]any {
if defs == nil {
return map[string]any{"read": 0, "not_asked": "the definitions were not read"}
}
var current, older []string
for _, n := range defs.Names.Current {
current = append(current, shownName(n))
}
for _, n := range defs.Names.Older {
older = append(older, shownName(n))
}
out := map[string]any{"read": defs.Read, "store_names": nonNil(current), "older_store_names": nonNil(older)}
if defs.NotAsked != "" {
out["not_asked"] = defs.NotAsked
}
if defs.NamesNotRead != "" {
out["older_store_names_not_read"] = defs.NamesNotRead
}
if len(defs.Unread) > 0 {
out["unread"] = defs.Unread
}
return out
}
// definitionsWarning says, when any definition was not read, that a manifest it pins reads as unrecorded.
func definitionsWarning(defs *Definitions) string {
switch {
case defs == nil:
return ""
case defs.NotAsked != "":
return "the definitions could not be read (" + defs.NotAsked + "), so a manifest a definition pins " +
"and no record names reads as unrecorded here: ask again before reading unrecorded as unused"
case defs.NamesNotRead != "":
return defs.NamesNotRead + ", so a manifest a definition pins under an older name of the store and no " +
"record names reads as unrecorded here"
case len(defs.Unread) > 0:
return fmt.Sprintf("%d definitions could not be read (listed in definitions.unread), so a manifest one "+
"of them pins and no record names reads as unrecorded here: ask again — what was read is kept", len(defs.Unread))
}
return ""
}
// Collected answers artifact_store_collect: the controller's answer, and what the store's collector frees.
func Collected(v *View, a *CollectAnswer, dry bool, entries []Entry) map[string]any {
refs := a.LetGo
@@ -0,0 +1,4 @@
# The names the artifact store had before, from the setting older-store-names, written by the mesh.
# artifact_store_references reads every definition under these too (novox/hq issue 323).
# Replaced on every push; change the setting, never this file.
older=${config:older-store-names}
+35 -2
View File
@@ -3,13 +3,40 @@
"version": "1",
"invokes": [
"seat:mesh-controller.artifacts",
"seat:mesh-controller.collect"
"seat:mesh-controller.collect",
"mesh-catalog.catalog_modules",
"mesh-catalog.catalog_module"
],
"tools": [
"artifact_store_repositories",
"artifact_store_usage",
"artifact_store_references",
"artifact_store_collect"
],
"config": {
"older-store-names": {
"type": "string",
"description": "The names the artifact store had before, as host names or host:port separated by spaces, or none when it had no other. artifact_store_references reads every definition under these as well as under the store's current name, so an image a definition pins under one is said to be named by a definition (novox/hq issue 323).",
"pattern": "none|[A-Za-z0-9.:-]+( [A-Za-z0-9.:-]+)*",
"default": "none"
}
},
"resources": [
{
"id": "state",
"type": "directory",
"mode": "0700",
"place": ".",
"unknown": "program"
},
{
"id": "store-names",
"type": "file",
"path": "${dir:state}/store-names",
"mode": "0600",
"trusted": false,
"template": "files/store-names"
}
],
"build": {
"artifacts": [
@@ -24,8 +51,14 @@
"artifact-store-tools"
],
"env": {
"MESH_ARTIFACT_STORE_CONTAINER": "mesh-registry"
"MESH_ARTIFACT_STORE_CONTAINER": "mesh-registry",
"MESH_ARTIFACT_STORE_NAMES_FILE": "${dir:state}/store-names"
}
},
{
"name": "files",
"kind": "archive",
"from": "files"
}
]
}
+12
View File
@@ -385,3 +385,15 @@ The module no longer writes a file into i3's `config.d`. Its window-manager line
under `files/i3/` where it had one) are a contribution to `node-display-session`. The i3 module places
them in its own configuration under a `# <module>` line, so this module depends on a window manager
being assigned beside it.
## Files it does not know
Each directory or archive this module declares says how it treats a file it did not write (ADR 0422, ADR 0416).
- `/usr/local/lib/asus-zephyrus-g14`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
- `/etc/systemd/system/systemd-suspend.service.d`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
- `/etc/systemd/system/systemd-hibernate.service.d`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
- `/etc/systemd/system/systemd-suspend-then-hibernate.service.d`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
- `/etc/systemd/system/nvidia-powerd.service.d`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
- `/etc/UPower/UPower.conf.d`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
- `/etc/X11/xorg.conf.d`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
@@ -34,11 +34,9 @@ func TestKeysListsTriggersI3LinesAndFirmwareKeys(t *testing.T) {
home := "/home/operator"
t.Setenv("MESH_OPERATOR_HOME", home)
// The i3 module's file as the controller composes it: other modules' lines around this one's.
var own string
for _, c := range m.Contributions {
if c.Seat == "node-display-session" {
own += c.Content
}
own := m.i3Lines()
if own == "" || len(m.Contributions) != 0 {
t.Fatalf("the lines for the window manager are one item, and no contribution is left: %q %v", own, m.Contributions)
}
put(t, f.root, home+"/"+I3Config, "set $mod Mod4\n# rofi\nbindsym $mod+d exec rofi\n# "+ModuleName+"\n"+own+
"# triggerhappy-not-here\nbindsym $mod+Shift+z exec other\n#########################################\ninclude x\n")
@@ -15,19 +15,41 @@ type manifest struct {
Tools []string `json:"tools"`
Emits []string `json:"emits"`
Resources []map[string]any `json:"resources"`
// Contributions to other modules' seats (novox/hq ADR 0212): the vendor keys go to node-hotkeys.
Contributions []struct {
Seat string `json:"seat"`
Kind string `json:"kind"`
Content string `json:"content"`
} `json:"contributions"`
// Items of collections (novox/hq ADR 0349 clause 13): the vendor keys are an item of the node-hotkeys
// triggers, its text the template file beside the manifest.
Items []struct {
Collection string `json:"collection"`
Template string `json:"template"`
} `json:"items"`
// Old-form contributions to other modules' seats (novox/hq ADR 0212): none is left.
Contributions []any `json:"contributions"`
}
// triggers is the module's contribution to node-hotkeys: its vendor keys.
// i3Lines is the module's lines for the window manager: the text of the template of its item of the collection
// contribution:node-display-session:config (design 57 L3, issue 848).
func (m manifest) i3Lines() string {
for _, it := range m.Items {
if it.Collection == "contribution:node-display-session:config" {
raw, err := os.ReadFile(filepath.Join("..", "..", it.Template))
if err != nil {
return ""
}
return string(raw)
}
}
return ""
}
// triggers is the module's contribution to node-hotkeys: its vendor keys, the text of the template of its
// item of the collection contribution:node-hotkeys:trigger.
func (m manifest) triggers() string {
for _, c := range m.Contributions {
if c.Seat == "node-hotkeys" && c.Kind == "trigger" {
return c.Content
for _, it := range m.Items {
if it.Collection == "contribution:node-hotkeys:trigger" {
raw, err := os.ReadFile(filepath.Join("..", "..", it.Template))
if err != nil {
return ""
}
return string(raw)
}
}
return ""
@@ -0,0 +1,83 @@
package main
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
// Each file's text is a template in templates/, the directory the manifest names (novox/hq ADR 0349 §2, issue 731),
// never in files/: files/ is the archive the machine unpacks, and stays exactly what the machine is given.
func TestEveryFileTakesItsTextFromATemplateOutsideTheUnpackedFiles(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Templates string `json:"templates"`
Resources []map[string]any `json:"resources"`
Build struct {
Artifacts []map[string]any `json:"artifacts"`
} `json:"build"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
if m.Templates != "templates" {
t.Fatalf("the manifest names the templates directory %q, not templates", m.Templates)
}
files := 0
for _, r := range m.Resources {
if _, inline := r["content"]; inline {
t.Errorf("resource %v keeps its text inline: keep it in templates/ and name it as the template", r["id"])
}
if r["type"] != "file" {
continue
}
files++
name, _ := r["template"].(string)
if !strings.HasPrefix(name, "templates/") {
t.Errorf("resource %v names the template %q, not one in templates/", r["id"], name)
continue
}
if st, err := os.Lstat(filepath.Join("../..", name)); err != nil || !st.Mode().IsRegular() {
t.Errorf("resource %v names the template %s, which is not a plain file: %v", r["id"], name, err)
}
}
if files == 0 {
t.Fatal("no file resource")
}
packs := 0
for _, a := range m.Build.Artifacts {
if a["kind"] == "archive" && a["from"] == "templates" {
packs++
if a["name"] != "templates" {
t.Errorf("the templates archive is named %v, not templates", a["name"])
}
}
}
if packs != 1 {
t.Fatalf("the build packs templates/ into %d archives, not one", packs)
}
// The templates archive is read by the controller, never unpacked onto the machine.
for _, r := range m.Resources {
if r["artifact"] == "templates" {
t.Errorf("resource %v places the templates archive on the machine", r["id"])
}
}
// And files/ holds what the machine runs, no template.
err = filepath.WalkDir("../../files", func(at string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && !strings.HasPrefix(filepath.ToSlash(at), "../../files/bin/") {
t.Errorf("files/ holds %s, which is not one of the scripts the machine unpacks", at)
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
+46 -41
View File
@@ -1,6 +1,8 @@
{
"module": "asus-zephyrus-g14",
"version": "1",
"moved": true,
"templates": "templates",
"capabilities": [
"package-manager",
"service-manager"
@@ -52,80 +54,85 @@
"id": "scripts",
"type": "archive",
"path": "/usr/local/lib/asus-zephyrus-g14",
"artifact": "scripts"
"artifact": "scripts",
"unknown": "program"
},
{
"id": "nvidia-options",
"type": "file",
"path": "/etc/modprobe.d/g14-nvidia-power.conf",
"mode": "0644",
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The discrete GPU's driver options on the ROG Zephyrus G14 (GA403, RTX 40 series, hybrid graphics).\n#\n# NVreg_DynamicPowerManagement=0x00 turns runtime D3 off. With it on, a change of power source sends\n# the driver an ACPI notification it fails to handle on this model (\"RmHandleDNotifierEvent: Failed to\n# handle ACPI D-Notifier event, status=0x62\"), and the GPU stops making progress until the machine is\n# powered off. Off costs a few idle watts in hybrid mode and keeps the machine up.\n#\n# NVreg_PreserveVideoMemoryAllocations=1 saves video memory across suspend, so what used the GPU still\n# works after waking. It needs nvidia-suspend, -hibernate and -resume to run around a sleep, which this\n# module's drop-ins on the sleep services ask for.\n#\n# A change here applies when the driver next loads: at the next boot.\noptions nvidia NVreg_PreserveVideoMemoryAllocations=1\noptions nvidia NVreg_DynamicPowerManagement=0x00\n"
"template": "templates/modprobe.d/g14-nvidia-power.conf"
},
{
"id": "video-options",
"type": "file",
"path": "/etc/modprobe.d/video-brightness-switch.conf",
"mode": "0644",
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The ACPI video driver does not change the backlight itself on the brightness keys: on this model it\n# moves the wrong one. The keys are triggerhappy's (see /etc/triggerhappy/triggers.d/asus-g14.conf).\n# Applies when the module next loads: at the next boot.\noptions video brightness_switch_enabled=0\n"
"template": "templates/modprobe.d/video-brightness-switch.conf"
},
{
"id": "suspend-drop-ins",
"type": "directory",
"path": "/etc/systemd/system/systemd-suspend.service.d",
"mode": "0755"
"mode": "0755",
"unknown": "program"
},
{
"id": "nvidia-on-suspend",
"type": "file",
"path": "/etc/systemd/system/systemd-suspend.service.d/asus-zephyrus-g14-nvidia.conf",
"mode": "0644",
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The NVIDIA driver's own sleep actions, asked for by the sleep itself rather than enabled as\n# links: the mesh declares files and never makes links (novox/hq ADR 0012), and the host's service\n# shape must not start these units by hand, which would put the GPU to sleep with the machine awake.\n[Unit]\nWants=nvidia-suspend.service nvidia-resume.service\n"
"template": "templates/systemd/system/systemd-suspend.service.d/asus-zephyrus-g14-nvidia.conf"
},
{
"id": "hibernate-drop-ins",
"type": "directory",
"path": "/etc/systemd/system/systemd-hibernate.service.d",
"mode": "0755"
"mode": "0755",
"unknown": "program"
},
{
"id": "nvidia-on-hibernate",
"type": "file",
"path": "/etc/systemd/system/systemd-hibernate.service.d/asus-zephyrus-g14-nvidia.conf",
"mode": "0644",
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The NVIDIA driver's own sleep actions, asked for by the sleep itself rather than enabled as\n# links: the mesh declares files and never makes links (novox/hq ADR 0012), and the host's service\n# shape must not start these units by hand, which would put the GPU to sleep with the machine awake.\n[Unit]\nWants=nvidia-hibernate.service nvidia-resume.service\n"
"template": "templates/systemd/system/systemd-hibernate.service.d/asus-zephyrus-g14-nvidia.conf"
},
{
"id": "suspend-then-hibernate-drop-ins",
"type": "directory",
"path": "/etc/systemd/system/systemd-suspend-then-hibernate.service.d",
"mode": "0755"
"mode": "0755",
"unknown": "program"
},
{
"id": "nvidia-on-suspend-then-hibernate",
"type": "file",
"path": "/etc/systemd/system/systemd-suspend-then-hibernate.service.d/asus-zephyrus-g14-nvidia.conf",
"mode": "0644",
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The NVIDIA driver's own sleep actions, asked for by the sleep itself rather than enabled as\n# links: the mesh declares files and never makes links (novox/hq ADR 0012), and the host's service\n# shape must not start these units by hand, which would put the GPU to sleep with the machine awake.\n[Unit]\nWants=nvidia-suspend-then-hibernate.service nvidia-resume.service\n"
"template": "templates/systemd/system/systemd-suspend-then-hibernate.service.d/asus-zephyrus-g14-nvidia.conf"
},
{
"id": "powerd-drop-ins",
"type": "directory",
"path": "/etc/systemd/system/nvidia-powerd.service.d",
"mode": "0755"
"mode": "0755",
"unknown": "program"
},
{
"id": "powerd-opt-in",
"type": "file",
"path": "/etc/systemd/system/nvidia-powerd.service.d/asus-zephyrus-g14.conf",
"mode": "0644",
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# nvidia-powerd (Dynamic Boost) was the first error in the chain that hung this model's GPU on a change\n# of power source, and asusd starts it on mains. It runs only when the kernel command line says\n# zephyrus.nvidia-powerd — an explicit opt-in, at boot.\n[Unit]\nConditionKernelCommandLine=zephyrus.nvidia-powerd\n"
"template": "templates/systemd/system/nvidia-powerd.service.d/asus-zephyrus-g14.conf"
},
{
"id": "backlight-rule",
"type": "file",
"path": "/etc/udev/rules.d/90-backlight.rules",
"mode": "0644",
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The backlights are writable by the video group, so the brightness keys and the module's brightness tool\n# move the panel without root.\nACTION==\"add\", SUBSYSTEM==\"backlight\", RUN+=\"/usr/bin/chgrp video /sys/class/backlight/%k/brightness\", RUN+=\"/usr/bin/chmod g+w /sys/class/backlight/%k/brightness\"\n"
"template": "templates/udev/rules.d/90-backlight.rules"
},
{
"id": "udev",
@@ -144,14 +151,15 @@
"id": "upower-drop-ins",
"type": "directory",
"path": "/etc/UPower/UPower.conf.d",
"mode": "0755"
"mode": "0755",
"unknown": "program"
},
{
"id": "low-battery",
"type": "file",
"path": "/etc/UPower/UPower.conf.d/50-asus-zephyrus-g14.conf",
"mode": "0644",
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# On low battery the machine suspends rather than powering off, at 7 % — s2idle still draws a little,\n# so it leaves headroom. A drop-in over the package's own UPower.conf, which stays the package's.\n[UPower]\nUsePercentageForPolicy=true\nPercentageLow=15.0\nPercentageCritical=10.0\nPercentageAction=7.0\nCriticalPowerAction=Suspend\nAllowRiskyCriticalPowerAction=true\n"
"template": "templates/UPower/UPower.conf.d/50-asus-zephyrus-g14.conf"
},
{
"id": "upower",
@@ -167,14 +175,30 @@
"id": "xorg-drop-ins",
"type": "directory",
"path": "/etc/X11/xorg.conf.d",
"mode": "0755"
"mode": "0755",
"unknown": "program"
},
{
"id": "touchpad",
"type": "file",
"path": "/etc/X11/xorg.conf.d/30-asus-zephyrus-g14-touchpad.conf",
"mode": "0644",
"content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The touchpad's settings, applied by X every time the device appears — at login and after every\n# resume, when the device is initialised again. This replaces the predecessor's sleep hook, which ran\n# xinput after a resume as a named person on a guessed display.\nSection \"InputClass\"\n Identifier \"asus-zephyrus-g14 touchpad\"\n MatchIsTouchpad \"on\"\n Option \"Tapping\" \"on\"\n Option \"NaturalScrolling\" \"true\"\n Option \"AccelSpeed\" \"0.15\"\nEndSection\n"
"template": "templates/X11/xorg.conf.d/30-asus-zephyrus-g14-touchpad.conf"
}
],
"items": [
{
"collection": "shell:after-wake:normal",
"template": "templates/after-wake"
},
{
"collection": "contribution:node-hotkeys:trigger",
"template": "templates/hotkeys-triggers"
},
{
"collection": "contribution:node-display-session:config",
"name": "display-session",
"template": "templates/display-session"
}
],
"build": {
@@ -194,31 +218,12 @@
"name": "scripts",
"kind": "archive",
"from": "files"
},
{
"name": "templates",
"kind": "archive",
"from": "templates"
}
]
},
"contributions": [
{
"seat": "node-hotkeys",
"kind": "trigger",
"content": "# The ROG Zephyrus G14's vendor keys, which reach no X client: media (the M-keys), panel brightness,\n# and the touchpad key. Each runs this module's own script, as the operator's account.\nKEY_PROG1\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-media play-pause\nKEY_PROG3\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-media previous\nKEY_PROG4\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-media next\nKEY_BRIGHTNESSDOWN\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight -\nKEY_BRIGHTNESSDOWN\t2\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight -\nKEY_BRIGHTNESSUP\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight +\nKEY_BRIGHTNESSUP\t2\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight +\nKEY_F21\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset\n"
},
{
"seat": "node-display-session",
"kind": "config",
"content": "# The laptop's own lines in i3 (module asus-zephyrus-g14, novox/hq ADR 0208, ADR 0210). Owned by the\n# mesh: replaced at every push. Once the controller places contributions to node-display-session\n# (ADR 0210), these become the module's contribution instead of a file in i3's directory.\n#\n# The keys the firmware turns into ordinary key presses. The vendor keys that reach no X client are\n# triggerhappy's (/etc/triggerhappy/triggers.d/asus-g14.conf): M4 and Fn+F4/F5 for media, Fn+F7/F8 for\n# the panel, Fn+F10 for the touchpad. The keyboard backlight (Fn+F2/F3) is the firmware's and asusd's.\n\n# Fn+F6, the screenshot key: the firmware sends Super+Shift+S. Released before it runs, because the\n# screenshot grabs the pointer to select a region, which fails while the key is still held.\nbindsym --release $mod+Shift+s exec --no-startup-id $XDG_CONFIG_HOME/i3/scripts/screenshot.sh\n\n# Fn+F9, the display key: the firmware sends Super+P. Odd workspaces to the panel, even ones to the\n# external output.\nbindsym $mod+p exec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-display order\n\n# The keyboard backlight's level, shown when it changes.\nexec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-kbd-notify\n"
},
{
"seat": "node-display-session",
"kind": "config",
"content": "# The laptop's own lines in i3 (module asus-zephyrus-g14, novox/hq ADR 0208, ADR 0210). Owned by the\n# mesh: replaced at every push. Once the controller places contributions to node-display-session\n# (ADR 0210), these become the module's contribution instead of a file in i3's directory.\n#\n# The model's panel and touchpad.\n\n# The internal panel is the primary output, where the bars' tray goes. Which monitors are on and where\n# is the display server's (autorandr, run at every session start).\nexec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-display primary\n\n# The touchpad's settings again, by hand. X applies them itself whenever the device appears.\nbindsym $mod+Shift+x exec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset\n"
}
],
"shell": [
{
"for": "after-wake",
"slot": "normal",
"code": "# The touchpad's settings again after waking, in the operator's session: X applies the module's\n# input class when the device appears, and this covers a wake that does not initialise it again.\n# Runs as root from the power module; the reset itself runs as the session's owner.\nsleep 2\nowner=$(ps -o user= -C i3 | head -n 1)\n[ -n \"$owner\" ] && runuser -u \"$owner\" -- /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset\ntrue\n"
}
]
}
}
@@ -0,0 +1,11 @@
# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.
#
# On low battery the machine suspends rather than powering off, at 7 % — s2idle still draws a little,
# so it leaves headroom. A drop-in over the package's own UPower.conf, which stays the package's.
[UPower]
UsePercentageForPolicy=true
PercentageLow=15.0
PercentageCritical=10.0
PercentageAction=7.0
CriticalPowerAction=Suspend
AllowRiskyCriticalPowerAction=true
@@ -0,0 +1,12 @@
# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.
#
# The touchpad's settings, applied by X every time the device appears — at login and after every
# resume, when the device is initialised again. This replaces the predecessor's sleep hook, which ran
# xinput after a resume as a named person on a guessed display.
Section "InputClass"
Identifier "asus-zephyrus-g14 touchpad"
MatchIsTouchpad "on"
Option "Tapping" "on"
Option "NaturalScrolling" "true"
Option "AccelSpeed" "0.15"
EndSection
@@ -0,0 +1,7 @@
# The touchpad's settings again after waking, in the operator's session: X applies the module's
# input class when the device appears, and this covers a wake that does not initialise it again.
# Runs as root from the power module; the reset itself runs as the session's owner.
sleep 2
owner=$(ps -o user= -C i3 | head -n 1)
[ -n "$owner" ] && runuser -u "$owner" -- /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset
true
@@ -0,0 +1,30 @@
# The laptop's own lines in i3 (module asus-zephyrus-g14, novox/hq ADR 0208, ADR 0210). Owned by the
# mesh: replaced at every push. Once the controller places contributions to node-display-session
# (ADR 0210), these become the module's contribution instead of a file in i3's directory.
#
# The keys the firmware turns into ordinary key presses. The vendor keys that reach no X client are
# triggerhappy's (/etc/triggerhappy/triggers.d/asus-g14.conf): M4 and Fn+F4/F5 for media, Fn+F7/F8 for
# the panel, Fn+F10 for the touchpad. The keyboard backlight (Fn+F2/F3) is the firmware's and asusd's.
# Fn+F6, the screenshot key: the firmware sends Super+Shift+S. Released before it runs, because the
# screenshot grabs the pointer to select a region, which fails while the key is still held.
bindsym --release $mod+Shift+s exec --no-startup-id $XDG_CONFIG_HOME/i3/scripts/screenshot.sh
# Fn+F9, the display key: the firmware sends Super+P. Odd workspaces to the panel, even ones to the
# external output.
bindsym $mod+p exec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-display order
# The keyboard backlight's level, shown when it changes.
exec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-kbd-notify
# The laptop's own lines in i3 (module asus-zephyrus-g14, novox/hq ADR 0208, ADR 0210). Owned by the
# mesh: replaced at every push. Once the controller places contributions to node-display-session
# (ADR 0210), these become the module's contribution instead of a file in i3's directory.
#
# The model's panel and touchpad.
# The internal panel is the primary output, where the bars' tray goes. Which monitors are on and where
# is the display server's (autorandr, run at every session start).
exec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-display primary
# The touchpad's settings again, by hand. X applies them itself whenever the device appears.
bindsym $mod+Shift+x exec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset
@@ -0,0 +1,10 @@
# The ROG Zephyrus G14's vendor keys, which reach no X client: media (the M-keys), panel brightness,
# and the touchpad key. Each runs this module's own script, as the operator's account.
KEY_PROG1 1 /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-media play-pause
KEY_PROG3 1 /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-media previous
KEY_PROG4 1 /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-media next
KEY_BRIGHTNESSDOWN 1 /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight -
KEY_BRIGHTNESSDOWN 2 /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight -
KEY_BRIGHTNESSUP 1 /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight +
KEY_BRIGHTNESSUP 2 /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight +
KEY_F21 1 /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset
@@ -0,0 +1,16 @@
# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.
#
# The discrete GPU's driver options on the ROG Zephyrus G14 (GA403, RTX 40 series, hybrid graphics).
#
# NVreg_DynamicPowerManagement=0x00 turns runtime D3 off. With it on, a change of power source sends
# the driver an ACPI notification it fails to handle on this model ("RmHandleDNotifierEvent: Failed to
# handle ACPI D-Notifier event, status=0x62"), and the GPU stops making progress until the machine is
# powered off. Off costs a few idle watts in hybrid mode and keeps the machine up.
#
# NVreg_PreserveVideoMemoryAllocations=1 saves video memory across suspend, so what used the GPU still
# works after waking. It needs nvidia-suspend, -hibernate and -resume to run around a sleep, which this
# module's drop-ins on the sleep services ask for.
#
# A change here applies when the driver next loads: at the next boot.
options nvidia NVreg_PreserveVideoMemoryAllocations=1
options nvidia NVreg_DynamicPowerManagement=0x00
@@ -0,0 +1,6 @@
# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.
#
# The ACPI video driver does not change the backlight itself on the brightness keys: on this model it
# moves the wrong one. The keys are triggerhappy's (see /etc/triggerhappy/triggers.d/asus-g14.conf).
# Applies when the module next loads: at the next boot.
options video brightness_switch_enabled=0
@@ -0,0 +1,7 @@
# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.
#
# nvidia-powerd (Dynamic Boost) was the first error in the chain that hung this model's GPU on a change
# of power source, and asusd starts it on mains. It runs only when the kernel command line says
# zephyrus.nvidia-powerd — an explicit opt-in, at boot.
[Unit]
ConditionKernelCommandLine=zephyrus.nvidia-powerd
@@ -0,0 +1,7 @@
# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.
#
# The NVIDIA driver's own sleep actions, asked for by the sleep itself rather than enabled as
# links: the mesh declares files and never makes links (novox/hq ADR 0012), and the host's service
# shape must not start these units by hand, which would put the GPU to sleep with the machine awake.
[Unit]
Wants=nvidia-hibernate.service nvidia-resume.service
@@ -0,0 +1,7 @@
# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.
#
# The NVIDIA driver's own sleep actions, asked for by the sleep itself rather than enabled as
# links: the mesh declares files and never makes links (novox/hq ADR 0012), and the host's service
# shape must not start these units by hand, which would put the GPU to sleep with the machine awake.
[Unit]
Wants=nvidia-suspend-then-hibernate.service nvidia-resume.service
@@ -0,0 +1,7 @@
# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.
#
# The NVIDIA driver's own sleep actions, asked for by the sleep itself rather than enabled as
# links: the mesh declares files and never makes links (novox/hq ADR 0012), and the host's service
# shape must not start these units by hand, which would put the GPU to sleep with the machine awake.
[Unit]
Wants=nvidia-suspend.service nvidia-resume.service
@@ -0,0 +1,5 @@
# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.
#
# The backlights are writable by the video group, so the brightness keys and the module's brightness tool
# move the panel without root.
ACTION=="add", SUBSYSTEM=="backlight", RUN+="/usr/bin/chgrp video /sys/class/backlight/%k/brightness", RUN+="/usr/bin/chmod g+w /sys/class/backlight/%k/brightness"
+11
View File
@@ -0,0 +1,11 @@
# audit-logger
The `audit-logger` module of the mesh catalogue: declares directories on the node.
## Files it does not know
Each directory or archive this module declares says how it treats a file it did not write (ADR 0422, ADR 0416).
- `${dir:state}`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
- `${dir:trail}`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
- `${dir:spool}`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
+6 -3
View File
@@ -47,17 +47,20 @@
"id": "state",
"type": "directory",
"mode": "0700",
"place": "."
"place": ".",
"unknown": "program"
},
{
"id": "trail",
"type": "directory",
"mode": "0700"
"mode": "0700",
"unknown": "program"
},
{
"id": "spool",
"type": "directory",
"mode": "0700"
"mode": "0700",
"unknown": "program"
}
],
"capabilities": [
+11
View File
@@ -0,0 +1,11 @@
# baserow
The `baserow` module of the mesh catalogue: runs the container image `baserow/baserow`.
## Files it does not know
Each directory or archive this module declares says how it treats a file it did not write (ADR 0422, ADR 0416).
- `${dir:mesh-state}`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
- `${dir:state}`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
- `${dir:data}`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
+4
View File
@@ -0,0 +1,4 @@
{
"password": "${secret:admin}",
"host": "${bound:route:name}"
}
+7
View File
@@ -0,0 +1,7 @@
DATABASE_HOST=${bound:postgres-database:at}
DATABASE_PORT=${bound:postgres-database:port}
DATABASE_NAME=${bound:postgres-database:as}
DATABASE_USER=${bound:postgres-database:as}
DATABASE_PASSWORD_FILE=/run/secrets/database
DISABLE_EMBEDDED_PSQL=true
BASEROW_PUBLIC_URL=https://${bound:route:name}

Some files were not shown because too many files have changed in this diff Show More