Fixes issue 342 (a password is taken as a setting, and outranks the vault's), in this repository. Decision: the issue's 2026-10-11 decision under the operator's rule "a password is not a setting" (shape 1 refused by the controller, shape 2 ignored by the client and said once), with ADR 0326 (protected / takes-settings on a mergeable file).
What
Shape 1: baserow, grafana, letta, unifi keep password in their runtime-config as protected. The controller already refuses a layer that sets a protected key, naming the layer and the key, never the value: the mesh's layer sets "password" on runtime-config, and that module keeps "password" for itself — it is not settable (checked against mesh-controller main's ApplySettings with each manifest).
Shape 2: the gitea (token), home-assistant (token), icecast (password), influxdb (token), mailu (apiKey) and nextcloud (password) clients no longer read the secret from the settings file. A value there is ignored and said once on stderr: the setting <key> is ignored: it comes from the vault (key only, never the value). icecast now reads its admin password from the vault's file (MESH_ICECAST_ADMIN_PASSWORD_FILE = ${dir:state}/admin.secret, user admin as its config says); before, it had no source but a setting.
The catalogue check gains checks/secrets (merge-check.sh step 6): a mergeable file whose key holds ${secret:…} must keep that key protected, or leave it out of its takes-settings. checks/secrets/known lists the one nested case the controller cannot protect alone yet, searxng settings server.secret_key (server is a key the file takes for the operator's other values, and ADR 0326 §4 refuses a key both taken and protected); a stale line is refused, so the list only shrinks.
nodered was listed in the issue and is no longer affected: its config.json does not merge, so no setting reaches its token.
Not in this PR
A nested secret (searxng's server.secret_key) needs the controller to refuse a nested key, or to refuse any key a mergeable file fills with ${secret:…} on its own, as the decision states; that is mesh-controller's part.
No live secret value changes. Whether any layer holds one of these keys today was not read (reading a layer prints its values). The merge gate composes every machine with these manifests, so a layer that sets a now-protected password shows there as a refusal by layer and key; a layer holding a shape-2 key shows in that module's log line after rollout.
Tests (fail on main, pass here)
modules/{gitea,home-assistant,icecast,influxdb,mailu,nextcloud}/test/secret-setting.test.ts: the vault's value is used with a setting present, the line is said once over two builds, and never with the value. On main all six fail.
checks/secrets: unit tests, plus the catalogue itself; on main it names baserow, grafana, letta and unifi.
MESH_GATE=<controller from mesh-controller main 8f1aec0> MESH_CHECK_MODULES=gitea,home-assistant,icecast,influxdb,mailu,nextcloud,baserow,grafana,letta,unifi,searxng sh merge-check.sh passes (67 resources without health, as kept); merge-check-typescript.sh for the same modules passes (gitea 85, home-assistant 15, influxdb 12, icecast/mailu/nextcloud 1 each).
Reviewers' checklist: acceptance (no module here takes a secret from a setting; a setting naming one is refused or ignored and said); tests fail without the fix; no value printed anywhere; manifests consistent (protected only on mergeable files, none also in takes-settings); checks green.
Fixes issue 342 (a password is taken as a setting, and outranks the vault's), in this repository. Decision: the issue's 2026-10-11 decision under the operator's rule "a password is not a setting" (shape 1 refused by the controller, shape 2 ignored by the client and said once), with ADR 0326 (`protected` / `takes-settings` on a mergeable file).
**What**
- Shape 1: `baserow`, `grafana`, `letta`, `unifi` keep `password` in their `runtime-config` as `protected`. The controller already refuses a layer that sets a protected key, naming the layer and the key, never the value: `the mesh's layer sets "password" on runtime-config, and that module keeps "password" for itself — it is not settable` (checked against mesh-controller main's `ApplySettings` with each manifest).
- Shape 2: the `gitea` (`token`), `home-assistant` (`token`), `icecast` (`password`), `influxdb` (`token`), `mailu` (`apiKey`) and `nextcloud` (`password`) clients no longer read the secret from the settings file. A value there is ignored and said once on stderr: `the setting <key> is ignored: it comes from the vault` (key only, never the value). icecast now reads its admin password from the vault's file (`MESH_ICECAST_ADMIN_PASSWORD_FILE` = `${dir:state}/admin.secret`, user `admin` as its config says); before, it had no source but a setting.
- The catalogue check gains `checks/secrets` (merge-check.sh step 6): a mergeable file whose key holds `${secret:…}` must keep that key `protected`, or leave it out of its `takes-settings`. `checks/secrets/known` lists the one nested case the controller cannot protect alone yet, `searxng settings server.secret_key` (`server` is a key the file takes for the operator's other values, and ADR 0326 §4 refuses a key both taken and protected); a stale line is refused, so the list only shrinks.
- `nodered` was listed in the issue and is no longer affected: its `config.json` does not merge, so no setting reaches its token.
**Not in this PR**
- A nested secret (searxng's `server.secret_key`) needs the controller to refuse a nested key, or to refuse any key a mergeable file fills with `${secret:…}` on its own, as the decision states; that is mesh-controller's part.
- No live secret value changes. Whether any layer holds one of these keys today was not read (reading a layer prints its values). The merge gate composes every machine with these manifests, so a layer that sets a now-protected `password` shows there as a refusal by layer and key; a layer holding a shape-2 key shows in that module's log line after rollout.
**Tests** (fail on main, pass here)
- `modules/{gitea,home-assistant,icecast,influxdb,mailu,nextcloud}/test/secret-setting.test.ts`: the vault's value is used with a setting present, the line is said once over two builds, and never with the value. On main all six fail.
- `checks/secrets`: unit tests, plus the catalogue itself; on main it names baserow, grafana, letta and unifi.
- `MESH_GATE=<controller from mesh-controller main 8f1aec0> MESH_CHECK_MODULES=gitea,home-assistant,icecast,influxdb,mailu,nextcloud,baserow,grafana,letta,unifi,searxng sh merge-check.sh` passes (67 resources without health, as kept); `merge-check-typescript.sh` for the same modules passes (gitea 85, home-assistant 15, influxdb 12, icecast/mailu/nextcloud 1 each).
**Reviewers' checklist**: acceptance (no module here takes a secret from a setting; a setting naming one is refused or ignored and said); tests fail without the fix; no value printed anywhere; manifests consistent (`protected` only on mergeable files, none also in `takes-settings`); checks green.
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…
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.
Deliverynovox/mesh-catalog@eb4280637906 — delivered since 2026-10-13T05:59:21Z
Delivery plan — builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi → ace, novox; no bus step; 1 wait(s) for a person
novox: receives gitea, nextcloud; waits for a person: mailu
baserow keeps data (ADR 0233): what it holds is backed up before it moves
gitea provides npm-package-registry, git: its consumers are sent again after it
gitea keeps data (ADR 0233): what it holds is backed up before it moves
grafana keeps data (ADR 0233): what it holds is backed up before it moves
home-assistant keeps data (ADR 0233): what it holds is backed up before it moves
icecast keeps data (ADR 0233): what it holds is backed up before it moves
influxdb provides influxdb-api: its consumers are sent again after it
influxdb keeps data (ADR 0233): what it holds is backed up before it moves
mailu waits for a person: its policy records (module)
mailu provides smtp: its consumers are sent again after it
mailu keeps data (ADR 0233): what it holds is backed up before it moves
nextcloud keeps data (ADR 0233): what it holds is backed up before it moves
unifi keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before baserow, grafana, home-assistant, icecast, influxdb, letta, unifi is judged on its first machine
composed together: pass — every machine composes with the change as it did without (4 of 4 compose)
Machines
grafana on ace: passed — carried by release-1791870888064652239
home-assistant on ace: passed — carried by release-1791870888064652239
icecast on ace: passed — carried by release-1791870888064652239
gitea on novox: passed — carried by release-1791870888064652239
baserow on ace: passed — carried by release-1791871008146737943
influxdb on ace: passed — carried by release-1791871008146737943
letta on ace: passed — carried by release-1791871008146737943
bazarr on ace: passed — judged with baserow: healthy 1 times over 1m18s
jackett on ace: passed — judged with baserow: healthy 1 times over 1m18s
kometa on ace: passed — judged with baserow: healthy 1 times over 1m18s
ombi on ace: passed — judged with baserow: healthy 1 times over 1m18s
qbittorrent on ace: passed — judged with baserow: healthy 1 times over 1m18s
tautulli on ace: passed — judged with baserow: healthy 1 times over 1m18s
unifi on ace: passed — judged with baserow: healthy 1 times over 1m18s
nextcloud on novox: passed — judged with gitea: healthy 1 times over 33s
baserow on the rest running it: sent
bazarr on the rest running it: sent
gitea on the rest running it: sent
grafana on the rest running it: sent
home-assistant on the rest running it: sent
icecast on the rest running it: sent
influxdb on the rest running it: sent
jackett on the rest running it: sent
kometa on the rest running it: sent
letta on the rest running it: sent
nextcloud on the rest running it: sent
ombi on the rest running it: sent
qbittorrent on the rest running it: sent
tautulli on the rest running it: sent
unifi on the rest running it: sent
Transitions
2026-10-13 05:43 (new) → proposed (announced): novox/mesh-catalog#264's head announced
2026-10-13 05:46 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
2026-10-13 05:46 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
2026-10-13 05:53 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
2026-10-13 05:53 published → delivering (go): its walk started: let go by this owner in its group's order, by a person, or on the controller's own path — not a failed walk that carried a later commit for it, after which it waits for a walk of its own commit
2026-10-13 05:59 delivering → delivered (done): its walk is done, carrying its own commit: every machine of its deploy plan passed, or was left as its policy says
The commit's note under refs/notes/mesh-plan keeps every transition: git log --notes=mesh-plan.
<!-- mesh-delivery:view -->
**Delivery** `novox/mesh-catalog@eb4280637906` — **delivered** since 2026-10-13T05:59:21Z
**Delivery plan** — builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi → ace, novox; no bus step; 1 wait(s) for a person
- tier 0: baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi
- ace: receives baserow, grafana, home-assistant, icecast, influxdb, letta, unifi
- novox: receives gitea, nextcloud; waits for a person: mailu
- baserow keeps data (ADR 0233): what it holds is backed up before it moves
- gitea provides npm-package-registry, git: its consumers are sent again after it
- gitea keeps data (ADR 0233): what it holds is backed up before it moves
- grafana keeps data (ADR 0233): what it holds is backed up before it moves
- home-assistant keeps data (ADR 0233): what it holds is backed up before it moves
- icecast keeps data (ADR 0233): what it holds is backed up before it moves
- influxdb provides influxdb-api: its consumers are sent again after it
- influxdb keeps data (ADR 0233): what it holds is backed up before it moves
- mailu waits for a person: its policy records (module)
- mailu provides smtp: its consumers are sent again after it
- mailu keeps data (ADR 0233): what it holds is backed up before it moves
- nextcloud keeps data (ADR 0233): what it holds is backed up before it moves
- unifi keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before baserow, grafana, home-assistant, icecast, influxdb, letta, unifi is judged on its first machine
**Group** `fix/342-a-secret-is-never-a-setting`, in order: novox/mesh-catalog@eb4280637906 → novox/mesh-media-catalog@c06014fbf838
- composed together: pass — every machine composes with the change as it did without (4 of 4 compose)
**Machines**
- grafana on ace: passed — carried by release-1791870888064652239
- home-assistant on ace: passed — carried by release-1791870888064652239
- icecast on ace: passed — carried by release-1791870888064652239
- gitea on novox: passed — carried by release-1791870888064652239
- baserow on ace: passed — carried by release-1791871008146737943
- influxdb on ace: passed — carried by release-1791871008146737943
- letta on ace: passed — carried by release-1791871008146737943
- bazarr on ace: passed — judged with baserow: healthy 1 times over 1m18s
- jackett on ace: passed — judged with baserow: healthy 1 times over 1m18s
- kometa on ace: passed — judged with baserow: healthy 1 times over 1m18s
- ombi on ace: passed — judged with baserow: healthy 1 times over 1m18s
- qbittorrent on ace: passed — judged with baserow: healthy 1 times over 1m18s
- tautulli on ace: passed — judged with baserow: healthy 1 times over 1m18s
- unifi on ace: passed — judged with baserow: healthy 1 times over 1m18s
- nextcloud on novox: passed — judged with gitea: healthy 1 times over 33s
- baserow on the rest running it: sent
- bazarr on the rest running it: sent
- gitea on the rest running it: sent
- grafana on the rest running it: sent
- home-assistant on the rest running it: sent
- icecast on the rest running it: sent
- influxdb on the rest running it: sent
- jackett on the rest running it: sent
- kometa on the rest running it: sent
- letta on the rest running it: sent
- nextcloud on the rest running it: sent
- ombi on the rest running it: sent
- qbittorrent on the rest running it: sent
- tautulli on the rest running it: sent
- unifi on the rest running it: sent
**Transitions**
- 2026-10-13 05:43 (new) → proposed (announced): novox/mesh-catalog#264's head announced
- 2026-10-13 05:46 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
- 2026-10-13 05:46 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
- 2026-10-13 05:53 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
- 2026-10-13 05:53 published → delivering (go): its walk started: let go by this owner in its group's order, by a person, or on the controller's own path — not a failed walk that carried a later commit for it, after which it waits for a walk of its own commit
- 2026-10-13 05:59 delivering → delivered (done): its walk is done, carrying its own commit: every machine of its deploy plan passed, or was left as its policy says
The commit's note under `refs/notes/mesh-plan` keeps every transition: `git log --notes=mesh-plan`.
Merge check at fe2040c6
Judged: head fe2040c65ea2 merged into trunk 53e51864969f; read beside it: mesh-controller cd6757f1e3c8, mesh-controller-main 8f1aec0832b1, mesh-host 7a41e120635b, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi)
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; its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
Delivery plan — builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi → ace, novox; no bus step; 1 wait(s) for a person
novox: receives gitea, nextcloud; waits for a person: mailu
baserow keeps data (ADR 0233): what it holds is backed up before it moves
gitea provides npm-package-registry, git: its consumers are sent again after it
gitea keeps data (ADR 0233): what it holds is backed up before it moves
grafana keeps data (ADR 0233): what it holds is backed up before it moves
home-assistant keeps data (ADR 0233): what it holds is backed up before it moves
icecast keeps data (ADR 0233): what it holds is backed up before it moves
influxdb provides influxdb-api: its consumers are sent again after it
influxdb keeps data (ADR 0233): what it holds is backed up before it moves
mailu waits for a person: its policy records (module)
mailu provides smtp: its consumers are sent again after it
mailu keeps data (ADR 0233): what it holds is backed up before it moves
nextcloud keeps data (ADR 0233): what it holds is backed up before it moves
unifi keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before baserow, grafana, home-assistant, icecast, influxdb, letta, unifi is judged on its first machine
read by no module's build: checks/secrets/go.mod, checks/secrets/known, checks/secrets/main.go, checks/secrets/main_test.go, merge-check.sh
Run by the build seat on shanks as build-1791869565559543520 (builds --log build-1791869565559543520).
✔ the newest status of each context is kept, with what it said, when and by whom (19.762797ms)
✔ a commit merges only when every required status is a success; a warning blocks (0.701371ms)
✔ no rule requires nothing; a rule that cannot be read says nothing, never yes (0.295125ms)
✔ a commit is contained in another when the compare finds nothing on it the other lacks (0.937115ms)
✔ a note line is appended once, as the forge's user, in its own repository (10.447758ms)
✔ a note that cannot be written is said, never read as written (0.368856ms)
✔ one view per pull request, found by its marker; only the mesh's statuses (0.571239ms)
✔ a build machine taking the check is said on the head: building on which machine, since the local time (14.492182ms)
✔ the verdict heard while a 'building' write is in flight lands last and stays (6.648707ms)
[gitea] novox/hq@a6b08de8: the verdict of build-1791680941000000000 is an older run's than the check now pending; not written
✔ the verdict written while a start's read of the forge is in flight stays the newest status (0.965116ms)
✔ the verdict heard while a 'building' write still waits its turn: the 'building' is never written (0.514218ms)
✔ a start heard after the verdict writes nothing (0.534228ms)
✔ a start from before the check now pending (a run before a recheck, the stream's history) writes nothing (0.513148ms)
✔ an old run's verdict redelivered after a recheck's pending is not written over it (0.885854ms)
[gitea] novox/hq@a6b08de8: the verdict of build-1791680941000000000 is an older run's than the check the forge holds pending; not written
[gitea] novox/hq@a6b08de8: the verdict of build-1791680941000000000 is an older run's than the check now pending; not written
✔ an old run's verdict in flight when the recheck comes lands before the recheck's pending (2.950167ms)
✔ a rechecked head says where its new run is: building on which machine (0.764192ms)
✔ the recheck's own verdict is written, even asked a little before the pending by another machine's clock (0.570639ms)
✔ after a restart, an old run's verdict is weighed against the check the forge's history holds open (0.557789ms)
✔ a recheck the forge does not take fails its caller, and the head is as it was (0.451487ms)
✔ the status tool writes the merge check's pending through the writer, and the mesh's other statuses as they were (5.811023ms)
✔ when a check was asked is read from its id; the opening of the check now pending from the gate's history (0.292785ms)
✔ a head this process did not announce (it restarted) is said only while the forge holds its gate pending (0.391197ms)
✔ a repository as the mesh clones it is told from another (0.292455ms)
✔ gitea_api refuses the forge's administration, a branch's protection and the merge check's status, and sends nothing (7.536771ms)
✔ gitea_api refuses a file written to the default branch or one a rule guards, and writes one on a branch of its own (60.910288ms)
✔ a protection rule covers a branch as the forge decides it: classes, alternatives, a plain name in any case (issue 539) (18.873204ms)
✔ gitea_api still writes what an author writes (8.057699ms)
✔ gitea_branch_protection_set only strengthens (15.254415ms)
✔ gitea_merge_pull_request merges only a head whose required statuses all succeeded, and only that head (41.71745ms)
✔ gitea_merge_pull_request refuses a verdict judged against an earlier trunk, and asks the check once (140.110839ms)
✔ gitea_merge_pull_request merges only the head it is named, and refuses one that is not the head (16.669968ms)
✔ gitea_merge_pull_request is loud when the forge merged a head other than the one it was handed (14.091576ms)
✔ gitea_merge_pull_request says the merge happened when reading back what landed fails (26.656808ms)
✔ gitea_merge_pull_request reads back a fast-forward, and says it cannot read back a squash or a rebase (56.146131ms)
✔ gitea_delete_repo deletes only the tools' own user's repository, unguarded, untouched for 30 days, named in full (21.792409ms)
✔ gitea_api sends the path it judged, and refuses moving a pull request's base (4.428621ms)
✔ gitea_merge_pull_request merges only into a branch whose protection requires the checks (6.963301ms)
✔ a rule named for a branch is not made weaker than the glob rule that guards it now (13.257053ms)
✔ a repository owner or name is never another path (0.300745ms)
✔ the judges, without a forge (0.375736ms)
✔ gitea_api never acts as another user: sudo, in any case, and any query key it does not send are refused (1.800059ms)
✔ gitea_api never approves a pull request nor requests changes: those are the operator's (1.721087ms)
✔ no tool's caller text becomes another path or another user: `..` and `?sudo=` are refused before the forge (5.061231ms)
✔ every page of a pull request's files is read, though the forge caps a page at fifty (31.822581ms)
✔ a pass asks only the repositories that moved since the last look, every one before the first (0.403827ms)
✔ the directories above a merge's files that hold a module at the commit are said, the root never (11.749769ms)
✔ a lookup the forge refuses is thrown, not read as no module (3.682909ms)
✔ a renamed file is said under its old name too, as gone (novox/hq ADR 0267) (4.587304ms)
✔ a new head beyond the newest 20 open pull requests is announced, exactly once (51.160091ms)
✔ a pass pages until it reaches back past its floor, and no further (23.524118ms)
✔ the statuses asked for replace the rule's, and an empty list requires none (6.351312ms)
✔ a new rule refuses direct pushes unless asked; an administrator's override only when said (0.264774ms)
✔ each open pull request's new head is announced once, and nothing closed or merged (10.377237ms)
✔ a verdict is the head commit's status; an error is the forge's error, never a success (1.104228ms)
✔ each layer is its own status: the gate with the modules it judged, the repository's own check beside it (0.758342ms)
✔ no verdict sets warning on a required check; only a person's decision fails (1.104098ms)
✔ a commit status is set on the commit, under the merge check's context (38.748422ms)
✔ a change plan is the gate's result: said on the status and, when it builds something, as a comment (0.505458ms)
✔ a head announced is pending in the build queue, since the local time it was announced (0.252464ms)
✔ the vault's token is used, a setting of it is ignored and said once, without its value (2.989398ms)
✔ first start: mints with the admin account, keeps the token at 0600, asks for the tools' scopes only (35.311176ms)
✔ second start: reuses the kept token, mints nothing (17.112654ms)
✔ the forge rejects the kept token (its data was restored): minted afresh, once, and the call goes through (22.448031ms)
✔ a token kept with write:admin by an earlier build is minted again without it at the next start, and the forge drops the old one (27.078695ms)
✔ a kept token lacking a scope the tools need: the forge refuses the call for the scope, the token is re-minted with the whole list, and the call goes through (19.864198ms)
✔ the kept file is gone but the forge still holds a token by that name: replaced, not refused (18.749151ms)
✔ concurrent first calls share one mint (11.20836ms)
✔ the admin account is refused: said plainly, nothing kept, and the next call fails the same way rather than crashing (12.851527ms)
✔ one process shares one source per kept file — the watcher and the tools never renew against each other (0.993966ms)
✔ a second process finds the token the first renewed, and reuses it instead of minting over it (20.168763ms)
✔ a configured token wins, and is reported rather than minted over when the forge rejects it (2.701923ms)
✔ nothing to mint with and no token: the client says what is missing (0.208014ms)
[gitea] no tools — no Gitea token, and nothing to mint one with — set MESH_GITEA_TOKEN, or MESH_GITEA_ADMIN_USER, MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-Ax8rK4/state/token
✔ the tools register once there is a way to a token, and the first call mints it (9.256978ms)
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-diDLRh/state/token
✔ a pull request can be closed, read and cleaned up through the tools (22.664604ms)
[gitea] novox/mesh-host's main moved from a2c2c35bbbbb to d28ca95ccccc: 25 open pull request(s) into it put out of date, nothing run (novox/hq ADR 0323)
[gitea] novox/mesh-host's main moved from d28ca95ccccc to ffffffffffff: 0 open pull request(s) into it put out of date, nothing run (novox/hq ADR 0323)
[gitea] novox/mesh-host: whether a trunk moved could not be weighed, so it is weighed again on the next pass — the forge is away
[gitea] novox/mesh-host's main moved from a2c2c35bbbbb to d28ca95ccccc: 0 open pull request(s) into it put out of date, nothing run (novox/hq ADR 0323)
✔ both statuses say the head and the trunk commit the verdict judged, first, whatever the summary's length (1.570475ms)
✔ a trunk's move puts an open head's merge check out of date, naming both commits, unless a check is on its way (0.961295ms)
✔ a push to a trunk puts every open pull request into it out of date — twenty-five of them — and none into another (4.360309ms)
✔ a trunk that cannot be read is weighed again on the next pass, never recorded half (0.496598ms)
✔ the trunk's move takes its turn after a verdict in flight, and weighs what the forge holds then (0.519429ms)
✔ a verdict judged against a trunk newer than the pass's first read is not put out of date (0.310916ms)
✔ a verdict that landed out of date keeps its account on the pull request (0.265294ms)
ℹ tests 85
ℹ suites 0
ℹ pass 85
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 635.999967
type-checking modules/grafana (TypeScript)
testing modules/grafana (TypeScript)
✔ influxdb-api is optional, and route and oidc-client stay hard (0.707081ms)
✔ every file naming influxdb-api is delivered only while it is bound (0.230264ms)
✔ the container mounts the data source's directories, never a file that may be absent (0.216774ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 91.580628
type-checking modules/home-assistant (TypeScript)
testing modules/home-assistant (TypeScript)
✔ the Servarr APIs and mqtt-topic are optional, and route stays hard (1.608126ms)
✔ mqtt: the broker is asked first; a login it does not take is never written (1.83363ms)
✔ mqtt: ace's entry (127.0.0.1, luffy) is moved to the bound broker and login, every other setting kept (33.07833ms)
✔ mqtt: a new password alone is written (the digest tells) (0.354215ms)
✔ mqtt: Home Assistant's own connection test refusing saves nothing and fails loudly (0.328415ms)
✔ mqtt: a fresh Home Assistant gets an entry; a grant without the discovery topics is warned about (0.463977ms)
✔ mqtt: two entries, or a binding without a port, are refused rather than guessed (0.261734ms)
✔ servarr: the mesh's minted key is never written; the remedy names the accept (0.718952ms)
✔ servarr: ace's entry at 127.0.0.1 reaches the same Sonarr the mesh bound at ace.internal — left, and said (0.444357ms)
✔ servarr: an entry already at the bound URL is unchanged (0.482448ms)
✔ servarr: a working entry that reaches a different app is refused, and nothing is removed (0.483478ms)
✔ servarr: no entry — one is made at the bound URL through the user flow (0.351556ms)
✔ servarr: a reauth Home Assistant started is finished with the bound URL and key (0.280935ms)
✔ form values: suggested first, then default, sections nested (0.146752ms)
✔ the vault's token is used, a setting of it is ignored and said once, without its value (1.960762ms)
ℹ tests 15
ℹ suites 0
ℹ pass 15
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 148.48899
type-checking modules/icecast (TypeScript)
testing modules/icecast (TypeScript)
✔ the vault's password is used, a setting of it is ignored and said once, without its value (2.471029ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 97.656864
type-checking modules/influxdb (TypeScript)
testing modules/influxdb (TypeScript)
✔ what a contribution may ask for, and what is refused (1.938351ms)
✔ a reader is given one authorization, reading every bucket of the org, under the mesh's password (37.980208ms)
✔ a writer is allowed its own buckets only, and a missing one is made — never deleted (23.162581ms)
✔ applying the same grant again writes nothing (13.383954ms)
✔ a rotated password is set in place; a changed access remakes only the mesh's own (51.152759ms)
✔ an authorization disabled, re-passworded or deleted behind the mesh's back is not held, and is made whole (50.977226ms)
✔ holds only reads, and a bucket gone missing is not held rather than made (13.441445ms)
✔ an authorization of the same name the mesh did not make is refused, and left exactly as it was (9.422631ms)
✔ the predecessor's own v1 users and tokens are never touched (10.126442ms)
✔ an org the instance does not have, or a non-mesh name, makes nothing (2.295407ms)
✔ a withdrawn consumer's authorization is removed, and an absent one is not an error (12.314677ms)
✔ the vault's token is used, a setting of it is ignored and said once, without its value (1.90439ms)
ℹ tests 12
ℹ suites 0
ℹ pass 12
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 344.701046
type-checking modules/letta (TypeScript)
NOT TESTED: modules/letta is TypeScript and has no tests of its own
type-checking modules/mailu (TypeScript)
testing modules/mailu (TypeScript)
✔ the vault's apiKey is used, a setting of it is ignored and said once, without its value (1.986852ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 92.319326
type-checking modules/nextcloud (TypeScript)
testing modules/nextcloud (TypeScript)
✔ the vault's password is used, a setting of it is ignored and said once, without its value (1.90965ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 91.38687
type-checking modules/unifi (TypeScript)
NOT TESTED: modules/unifi is TypeScript and has no tests of its own
**Merge check** at `fe2040c6`
Judged: head `fe2040c65ea2` merged into trunk `53e51864969f`; read beside it: mesh-controller `cd6757f1e3c8`, mesh-controller-main `8f1aec0832b1`, mesh-host `7a41e120635b`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi)
- `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; its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
**Delivery plan** — builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi → ace, novox; no bus step; 1 wait(s) for a person
- tier 0: baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi
- ace: receives baserow, grafana, home-assistant, icecast, influxdb, letta, unifi
- novox: receives gitea, nextcloud; waits for a person: mailu
- baserow keeps data (ADR 0233): what it holds is backed up before it moves
- gitea provides npm-package-registry, git: its consumers are sent again after it
- gitea keeps data (ADR 0233): what it holds is backed up before it moves
- grafana keeps data (ADR 0233): what it holds is backed up before it moves
- home-assistant keeps data (ADR 0233): what it holds is backed up before it moves
- icecast keeps data (ADR 0233): what it holds is backed up before it moves
- influxdb provides influxdb-api: its consumers are sent again after it
- influxdb keeps data (ADR 0233): what it holds is backed up before it moves
- mailu waits for a person: its policy records (module)
- mailu provides smtp: its consumers are sent again after it
- mailu keeps data (ADR 0233): what it holds is backed up before it moves
- nextcloud keeps data (ADR 0233): what it holds is backed up before it moves
- unifi keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before baserow, grafana, home-assistant, icecast, influxdb, letta, unifi is judged on its first machine
- read by no module's build: checks/secrets/go.mod, checks/secrets/known, checks/secrets/main.go, checks/secrets/main_test.go, merge-check.sh
Run by the build seat on shanks as `build-1791869565559543520` (`builds --log build-1791869565559543520`).
```
✔ the newest status of each context is kept, with what it said, when and by whom (19.762797ms)
✔ a commit merges only when every required status is a success; a warning blocks (0.701371ms)
✔ no rule requires nothing; a rule that cannot be read says nothing, never yes (0.295125ms)
✔ a commit is contained in another when the compare finds nothing on it the other lacks (0.937115ms)
✔ a note line is appended once, as the forge's user, in its own repository (10.447758ms)
✔ a note that cannot be written is said, never read as written (0.368856ms)
✔ one view per pull request, found by its marker; only the mesh's statuses (0.571239ms)
✔ a build machine taking the check is said on the head: building on which machine, since the local time (14.492182ms)
✔ the verdict heard while a 'building' write is in flight lands last and stays (6.648707ms)
[gitea] novox/hq@a6b08de8: the verdict of build-1791680941000000000 is an older run's than the check now pending; not written
✔ the verdict written while a start's read of the forge is in flight stays the newest status (0.965116ms)
✔ the verdict heard while a 'building' write still waits its turn: the 'building' is never written (0.514218ms)
✔ a start heard after the verdict writes nothing (0.534228ms)
✔ a start from before the check now pending (a run before a recheck, the stream's history) writes nothing (0.513148ms)
✔ an old run's verdict redelivered after a recheck's pending is not written over it (0.885854ms)
[gitea] novox/hq@a6b08de8: the verdict of build-1791680941000000000 is an older run's than the check the forge holds pending; not written
[gitea] novox/hq@a6b08de8: the verdict of build-1791680941000000000 is an older run's than the check now pending; not written
✔ an old run's verdict in flight when the recheck comes lands before the recheck's pending (2.950167ms)
✔ a rechecked head says where its new run is: building on which machine (0.764192ms)
✔ the recheck's own verdict is written, even asked a little before the pending by another machine's clock (0.570639ms)
✔ after a restart, an old run's verdict is weighed against the check the forge's history holds open (0.557789ms)
✔ a recheck the forge does not take fails its caller, and the head is as it was (0.451487ms)
✔ the status tool writes the merge check's pending through the writer, and the mesh's other statuses as they were (5.811023ms)
✔ when a check was asked is read from its id; the opening of the check now pending from the gate's history (0.292785ms)
✔ a head this process did not announce (it restarted) is said only while the forge holds its gate pending (0.391197ms)
✔ a repository as the mesh clones it is told from another (0.292455ms)
✔ gitea_api refuses the forge's administration, a branch's protection and the merge check's status, and sends nothing (7.536771ms)
✔ gitea_api refuses a file written to the default branch or one a rule guards, and writes one on a branch of its own (60.910288ms)
✔ a protection rule covers a branch as the forge decides it: classes, alternatives, a plain name in any case (issue 539) (18.873204ms)
✔ gitea_api still writes what an author writes (8.057699ms)
✔ gitea_branch_protection_set only strengthens (15.254415ms)
✔ gitea_merge_pull_request merges only a head whose required statuses all succeeded, and only that head (41.71745ms)
✔ gitea_merge_pull_request refuses a verdict judged against an earlier trunk, and asks the check once (140.110839ms)
✔ gitea_merge_pull_request merges only the head it is named, and refuses one that is not the head (16.669968ms)
✔ gitea_merge_pull_request is loud when the forge merged a head other than the one it was handed (14.091576ms)
✔ gitea_merge_pull_request says the merge happened when reading back what landed fails (26.656808ms)
✔ gitea_merge_pull_request reads back a fast-forward, and says it cannot read back a squash or a rebase (56.146131ms)
✔ gitea_delete_repo deletes only the tools' own user's repository, unguarded, untouched for 30 days, named in full (21.792409ms)
✔ gitea_api sends the path it judged, and refuses moving a pull request's base (4.428621ms)
✔ gitea_merge_pull_request merges only into a branch whose protection requires the checks (6.963301ms)
✔ a rule named for a branch is not made weaker than the glob rule that guards it now (13.257053ms)
✔ a repository owner or name is never another path (0.300745ms)
✔ the judges, without a forge (0.375736ms)
✔ gitea_api never acts as another user: sudo, in any case, and any query key it does not send are refused (1.800059ms)
✔ gitea_api never approves a pull request nor requests changes: those are the operator's (1.721087ms)
✔ no tool's caller text becomes another path or another user: `..` and `?sudo=` are refused before the forge (5.061231ms)
✔ every page of a pull request's files is read, though the forge caps a page at fifty (31.822581ms)
✔ a pass asks only the repositories that moved since the last look, every one before the first (0.403827ms)
✔ the directories above a merge's files that hold a module at the commit are said, the root never (11.749769ms)
✔ a lookup the forge refuses is thrown, not read as no module (3.682909ms)
✔ a renamed file is said under its old name too, as gone (novox/hq ADR 0267) (4.587304ms)
✔ a new head beyond the newest 20 open pull requests is announced, exactly once (51.160091ms)
✔ a pass pages until it reaches back past its floor, and no further (23.524118ms)
✔ the statuses asked for replace the rule's, and an empty list requires none (6.351312ms)
✔ a new rule refuses direct pushes unless asked; an administrator's override only when said (0.264774ms)
✔ each open pull request's new head is announced once, and nothing closed or merged (10.377237ms)
✔ a verdict is the head commit's status; an error is the forge's error, never a success (1.104228ms)
✔ each layer is its own status: the gate with the modules it judged, the repository's own check beside it (0.758342ms)
✔ no verdict sets warning on a required check; only a person's decision fails (1.104098ms)
✔ a commit status is set on the commit, under the merge check's context (38.748422ms)
✔ a change plan is the gate's result: said on the status and, when it builds something, as a comment (0.505458ms)
✔ a head announced is pending in the build queue, since the local time it was announced (0.252464ms)
✔ the vault's token is used, a setting of it is ignored and said once, without its value (2.989398ms)
✔ first start: mints with the admin account, keeps the token at 0600, asks for the tools' scopes only (35.311176ms)
✔ second start: reuses the kept token, mints nothing (17.112654ms)
✔ the forge rejects the kept token (its data was restored): minted afresh, once, and the call goes through (22.448031ms)
✔ a token kept with write:admin by an earlier build is minted again without it at the next start, and the forge drops the old one (27.078695ms)
✔ a kept token lacking a scope the tools need: the forge refuses the call for the scope, the token is re-minted with the whole list, and the call goes through (19.864198ms)
✔ the kept file is gone but the forge still holds a token by that name: replaced, not refused (18.749151ms)
✔ concurrent first calls share one mint (11.20836ms)
✔ the admin account is refused: said plainly, nothing kept, and the next call fails the same way rather than crashing (12.851527ms)
✔ one process shares one source per kept file — the watcher and the tools never renew against each other (0.993966ms)
✔ a second process finds the token the first renewed, and reuses it instead of minting over it (20.168763ms)
✔ a configured token wins, and is reported rather than minted over when the forge rejects it (2.701923ms)
✔ nothing to mint with and no token: the client says what is missing (0.208014ms)
[gitea] no tools — no Gitea token, and nothing to mint one with — set MESH_GITEA_TOKEN, or MESH_GITEA_ADMIN_USER, MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-Ax8rK4/state/token
✔ the tools register once there is a way to a token, and the first call mints it (9.256978ms)
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-diDLRh/state/token
✔ a pull request can be closed, read and cleaned up through the tools (22.664604ms)
[gitea] novox/mesh-host's main moved from a2c2c35bbbbb to d28ca95ccccc: 25 open pull request(s) into it put out of date, nothing run (novox/hq ADR 0323)
[gitea] novox/mesh-host's main moved from d28ca95ccccc to ffffffffffff: 0 open pull request(s) into it put out of date, nothing run (novox/hq ADR 0323)
[gitea] novox/mesh-host: whether a trunk moved could not be weighed, so it is weighed again on the next pass — the forge is away
[gitea] novox/mesh-host's main moved from a2c2c35bbbbb to d28ca95ccccc: 0 open pull request(s) into it put out of date, nothing run (novox/hq ADR 0323)
✔ both statuses say the head and the trunk commit the verdict judged, first, whatever the summary's length (1.570475ms)
✔ a trunk's move puts an open head's merge check out of date, naming both commits, unless a check is on its way (0.961295ms)
✔ a push to a trunk puts every open pull request into it out of date — twenty-five of them — and none into another (4.360309ms)
✔ a trunk that cannot be read is weighed again on the next pass, never recorded half (0.496598ms)
✔ the trunk's move takes its turn after a verdict in flight, and weighs what the forge holds then (0.519429ms)
✔ a verdict judged against a trunk newer than the pass's first read is not put out of date (0.310916ms)
✔ a verdict that landed out of date keeps its account on the pull request (0.265294ms)
ℹ tests 85
ℹ suites 0
ℹ pass 85
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 635.999967
type-checking modules/grafana (TypeScript)
testing modules/grafana (TypeScript)
✔ influxdb-api is optional, and route and oidc-client stay hard (0.707081ms)
✔ every file naming influxdb-api is delivered only while it is bound (0.230264ms)
✔ the container mounts the data source's directories, never a file that may be absent (0.216774ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 91.580628
type-checking modules/home-assistant (TypeScript)
testing modules/home-assistant (TypeScript)
✔ the Servarr APIs and mqtt-topic are optional, and route stays hard (1.608126ms)
✔ mqtt: the broker is asked first; a login it does not take is never written (1.83363ms)
✔ mqtt: ace's entry (127.0.0.1, luffy) is moved to the bound broker and login, every other setting kept (33.07833ms)
✔ mqtt: a new password alone is written (the digest tells) (0.354215ms)
✔ mqtt: Home Assistant's own connection test refusing saves nothing and fails loudly (0.328415ms)
✔ mqtt: a fresh Home Assistant gets an entry; a grant without the discovery topics is warned about (0.463977ms)
✔ mqtt: two entries, or a binding without a port, are refused rather than guessed (0.261734ms)
✔ servarr: the mesh's minted key is never written; the remedy names the accept (0.718952ms)
✔ servarr: ace's entry at 127.0.0.1 reaches the same Sonarr the mesh bound at ace.internal — left, and said (0.444357ms)
✔ servarr: an entry already at the bound URL is unchanged (0.482448ms)
✔ servarr: a working entry that reaches a different app is refused, and nothing is removed (0.483478ms)
✔ servarr: no entry — one is made at the bound URL through the user flow (0.351556ms)
✔ servarr: a reauth Home Assistant started is finished with the bound URL and key (0.280935ms)
✔ form values: suggested first, then default, sections nested (0.146752ms)
✔ the vault's token is used, a setting of it is ignored and said once, without its value (1.960762ms)
ℹ tests 15
ℹ suites 0
ℹ pass 15
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 148.48899
type-checking modules/icecast (TypeScript)
testing modules/icecast (TypeScript)
✔ the vault's password is used, a setting of it is ignored and said once, without its value (2.471029ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 97.656864
type-checking modules/influxdb (TypeScript)
testing modules/influxdb (TypeScript)
✔ what a contribution may ask for, and what is refused (1.938351ms)
✔ a reader is given one authorization, reading every bucket of the org, under the mesh's password (37.980208ms)
✔ a writer is allowed its own buckets only, and a missing one is made — never deleted (23.162581ms)
✔ applying the same grant again writes nothing (13.383954ms)
✔ a rotated password is set in place; a changed access remakes only the mesh's own (51.152759ms)
✔ an authorization disabled, re-passworded or deleted behind the mesh's back is not held, and is made whole (50.977226ms)
✔ holds only reads, and a bucket gone missing is not held rather than made (13.441445ms)
✔ an authorization of the same name the mesh did not make is refused, and left exactly as it was (9.422631ms)
✔ the predecessor's own v1 users and tokens are never touched (10.126442ms)
✔ an org the instance does not have, or a non-mesh name, makes nothing (2.295407ms)
✔ a withdrawn consumer's authorization is removed, and an absent one is not an error (12.314677ms)
✔ the vault's token is used, a setting of it is ignored and said once, without its value (1.90439ms)
ℹ tests 12
ℹ suites 0
ℹ pass 12
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 344.701046
type-checking modules/letta (TypeScript)
NOT TESTED: modules/letta is TypeScript and has no tests of its own
type-checking modules/mailu (TypeScript)
testing modules/mailu (TypeScript)
✔ the vault's apiKey is used, a setting of it is ignored and said once, without its value (1.986852ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 92.319326
type-checking modules/nextcloud (TypeScript)
testing modules/nextcloud (TypeScript)
✔ the vault's password is used, a setting of it is ignored and said once, without its value (1.90965ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 91.38687
type-checking modules/unifi (TypeScript)
NOT TESTED: modules/unifi is TypeScript and has no tests of its own
```
Independent review for hq issue 342 (its 2026-10-11 decision) and ADR 0326. Statuses at this head, read 2026-10-13 07:40: mesh/merge-gate success ("head fe2040c65e merged into trunk 53e5186496 — pass: builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, n…", 4 of 4 machines compose), mesh/repo-check success ("head fe2040c65e merged into trunk 53e5186496 — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh)…"), mesh/delivery success ("ready: it delivers once merged"), mesh/delivery-group success ("group fix/342-a-secret-is-never-a-setting ready: every member ready, and composed together they pass").
BLOCKING
A setting can still replace grafana's credential. modules/grafana/client.ts reads const token = cfg.token ?? env.MESH_GRAFANA_TOKEN and, when it is set, authenticates with it as a Bearer token instead of the vault's admin password. grafana's runtime-config merges JSON, names no takes-settings, and so takes every key of the mesh-wide and node layers (ADR 0326 §3, today's form). token is not in the file's content and is not protected, so a layer setting token lands in config.json and outranks the vault. Protecting password alone does not meet the acceptance test ("no module takes a password, token or API key from a setting"). Fix it as shape 2: ignore cfg.token (and keep reading cfg.password only from the protected file) with the same said-once line, and add a test. The same reading applies to baserow, letta and unifi: their clients read cfg.password first, which is now protected, and no other secret key, so they are fine.
checks/secrets/known can grow, although the PR and the check's comment say it only shrinks. Judge refuses a stale line, but a new finding with a new line beside it passes. I checked this: removing protected from grafana's runtime-config and adding grafana runtime-config password to known makes go run . ../.. print "no setting can replace a vault secret in a mergeable file (2 known, nested)" and exit 0. merge-check.sh judges checks/paths/known against the base branch's copy (base_lists), but it does not do that for checks/secrets/known. Judge it against origin/${MESH_CHECK_BASE:-main}:checks/secrets/known the same way, refusing a line the base does not have, and add a test for it.
NOT BLOCKING
The tests fail on main and pass at this head, which I checked locally. With main's client.ts, module.json and package.json and the head's tests, merge-check-typescript.sh fails one test in each of gitea, home-assistant, icecast, influxdb, mailu and nextcloud, and checks/secrets names baserow, grafana, letta and unifi. At the head, MESH_GATE=<mesh-controller main 8f1aec0> MESH_CHECK_MODULES=gitea,home-assistant,icecast,influxdb,mailu,nextcloud,baserow,grafana,letta,unifi,searxng sh merge-check.sh exits 0 ("long-running resources without health: 67, as kept"), and merge-check-typescript.sh for the same modules exits 0.
The ignored line names the key only. Every new ignoreSecretSetting prints the setting <key> is ignored: it comes from the vault and never the value, and the tests assert that the value is absent.
No lockout follows from the manifests. Reasoning from the manifests: icecast's admin password is a required provision secret already rendered into icecast.xml, and nextcloud uses the same tool-side path (${dir:state}/admin.secret). gitea falls back to its minted token, influxdb's and nextcloud's own secrets are minted and given to the application, mailu's api-token is the provision secret given to mailu as API_TOKEN, and home-assistant's provisions already need the accepted token file. For the four protected files, the gate composing 4 of 4 machines "as it did without" shows that no live layer sets a now-protected password today. The decision asks the pull request to say how many layers and where, so state that count (0, by the gate) in the description. One caution: baserow's admin and unifi's controller are minted secrets that the manifests never give to Baserow or UniFi, whose accounts are made in their own UIs. Their tools work only if the operator accepted the real password into the vault. This PR does not make that worse, but it is worth one line in the description.
Small points. home-assistant's error still says "set MESH_HOMEASSISTANT_TOKEN", while the real remedy is to accept the token own secret. The secrets check does not see a client that reads a secret key absent from the file's content (finding 1); say that limit in the check's comment. searxng's nested server.secret_key stays open until the controller can protect a nested key. Name the follow-up issue for that, so the known line has an owner.
Verdict: request changes at fe2040c65ea2fbb5ba66a9f21bc4f73be9383b98
Independent review for hq issue 342 (its 2026-10-11 decision) and ADR 0326. Statuses at this head, read 2026-10-13 07:40: `mesh/merge-gate` success ("head fe2040c65ea2 merged into trunk 53e51864969f — pass: builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, n…", 4 of 4 machines compose), `mesh/repo-check` success ("head fe2040c65ea2 merged into trunk 53e51864969f — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh)…"), `mesh/delivery` success ("ready: it delivers once merged"), `mesh/delivery-group` success ("group fix/342-a-secret-is-never-a-setting ready: every member ready, and composed together they pass").
BLOCKING
1. A setting can still replace grafana's credential. `modules/grafana/client.ts` reads `const token = cfg.token ?? env.MESH_GRAFANA_TOKEN` and, when it is set, authenticates with it as a Bearer token instead of the vault's admin password. grafana's `runtime-config` merges JSON, names no `takes-settings`, and so takes every key of the mesh-wide and node layers (ADR 0326 §3, today's form). `token` is not in the file's content and is not protected, so a layer setting `token` lands in config.json and outranks the vault. Protecting `password` alone does not meet the acceptance test ("no module takes a password, token or API key from a setting"). Fix it as shape 2: ignore `cfg.token` (and keep reading `cfg.password` only from the protected file) with the same said-once line, and add a test. The same reading applies to baserow, letta and unifi: their clients read `cfg.password` first, which is now protected, and no other secret key, so they are fine.
2. `checks/secrets/known` can grow, although the PR and the check's comment say it only shrinks. `Judge` refuses a stale line, but a new finding with a new line beside it passes. I checked this: removing `protected` from grafana's `runtime-config` and adding `grafana runtime-config password` to `known` makes `go run . ../..` print "no setting can replace a vault secret in a mergeable file (2 known, nested)" and exit 0. merge-check.sh judges `checks/paths/known` against the base branch's copy (`base_lists`), but it does not do that for `checks/secrets/known`. Judge it against `origin/${MESH_CHECK_BASE:-main}:checks/secrets/known` the same way, refusing a line the base does not have, and add a test for it.
NOT BLOCKING
3. The tests fail on main and pass at this head, which I checked locally. With main's `client.ts`, `module.json` and `package.json` and the head's tests, merge-check-typescript.sh fails one test in each of gitea, home-assistant, icecast, influxdb, mailu and nextcloud, and `checks/secrets` names baserow, grafana, letta and unifi. At the head, `MESH_GATE=<mesh-controller main 8f1aec0> MESH_CHECK_MODULES=gitea,home-assistant,icecast,influxdb,mailu,nextcloud,baserow,grafana,letta,unifi,searxng sh merge-check.sh` exits 0 ("long-running resources without health: 67, as kept"), and merge-check-typescript.sh for the same modules exits 0.
4. The ignored line names the key only. Every new `ignoreSecretSetting` prints `the setting <key> is ignored: it comes from the vault` and never the value, and the tests assert that the value is absent.
5. No lockout follows from the manifests. Reasoning from the manifests: icecast's admin password is a required provision secret already rendered into icecast.xml, and nextcloud uses the same tool-side path (`${dir:state}/admin.secret`). gitea falls back to its minted token, influxdb's and nextcloud's own secrets are minted and given to the application, mailu's `api-token` is the provision secret given to mailu as `API_TOKEN`, and home-assistant's provisions already need the accepted `token` file. For the four protected files, the gate composing 4 of 4 machines "as it did without" shows that no live layer sets a now-protected `password` today. The decision asks the pull request to say how many layers and where, so state that count (0, by the gate) in the description. One caution: baserow's `admin` and unifi's `controller` are minted secrets that the manifests never give to Baserow or UniFi, whose accounts are made in their own UIs. Their tools work only if the operator accepted the real password into the vault. This PR does not make that worse, but it is worth one line in the description.
6. Small points. home-assistant's error still says "set MESH_HOMEASSISTANT_TOKEN", while the real remedy is to accept the `token` own secret. The secrets check does not see a client that reads a secret key absent from the file's content (finding 1); say that limit in the check's comment. searxng's nested `server.secret_key` stays open until the controller can protect a nested key. Name the follow-up issue for that, so the `known` line has an owner.
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…
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.
Review 168 addressed at head eb42806379. (1) grafana: token is now protected beside password, and the client logs in with the vault's admin password first; a token in the settings file is ignored and said once by key (only MESH_GRAFANA_TOKEN from the environment is used, after the password). modules/grafana/test/secret-setting.test.ts fails at fe2040c6 (2 of 5) and passes here. (2) checks/secrets/known only shrinks: merge-check.sh reads the trunk's copy at the merge base and passes it with -base, and the check refuses any line the trunk's copy does not hold. Until the trunk holds a copy, a seed in the code (the searxng line) stands for it. Tests: TestALineTheTrunkDoesNotHoldIsRefused, TestTheCheckRefusesALineAddedAgainstTheTrunk. I reproduced the review's case: unprotecting letta's password and adding its line to known fails merge-check.sh. The nested-key follow-up is issue 610.
Review 168 addressed at head eb4280637906dfa117b16b71a3302a93e655be7f. (1) grafana: `token` is now protected beside `password`, and the client logs in with the vault's admin password first; a `token` in the settings file is ignored and said once by key (only `MESH_GRAFANA_TOKEN` from the environment is used, after the password). `modules/grafana/test/secret-setting.test.ts` fails at fe2040c6 (2 of 5) and passes here. (2) checks/secrets/known only shrinks: merge-check.sh reads the trunk's copy at the merge base and passes it with `-base`, and the check refuses any line the trunk's copy does not hold. Until the trunk holds a copy, a seed in the code (the searxng line) stands for it. Tests: `TestALineTheTrunkDoesNotHoldIsRefused`, `TestTheCheckRefusesALineAddedAgainstTheTrunk`. I reproduced the review's case: unprotecting letta's `password` and adding its line to `known` fails merge-check.sh. The nested-key follow-up is issue 610.
Re-review of review 168's two blocking findings. Statuses at this head, read 2026-10-13 07:45, are all still pending: mesh/merge-gate pending ("building on ace since 07:43"), mesh/delivery pending ("checking"), mesh/delivery-group pending ("group fix/342-a-secret-is-never-a-setting checking: 0 of 2 member(s) ready"). mesh/repo-check has not been posted at this head yet. This approval holds only once mesh/merge-gate and mesh/repo-check are success at this head and have been read. The gate also tests the new token protection on grafana: if it still composes 4 of 4 machines, no live layer sets grafana token.
RESOLVED
grafana. token is now protected beside password. The client logs in with the vault's admin password first, ignores a token in the settings file and says so once by key (the setting token is ignored: it comes from the vault), and uses only MESH_GRAFANA_TOKEN from the environment after the password. I ran the new grafana tests locally: 5 of 5 pass at this head, and against main's grafana client.ts and module.json 2 of 5 fail. A controller built from mesh-controller main (a688dcf) accepts a protected key that is absent from the file's content: MESH_GATE=<that controller> MESH_CHECK_MODULES=grafana,gitea,home-assistant,icecast,influxdb,mailu,nextcloud,baserow,letta,unifi,searxng sh merge-check.sh exits 0, with "long-running resources without health: 67, as kept" and "secrets: ... (1 known, nested)".
The known list now only shrinks. I repeated my own failing case: I removed password from grafana's protected, added grafana runtime-config password to known, committed, and ran merge-check.sh. It now exits 1, refused by TestALineTheTrunkDoesNotHoldIsRefused ("known holds lines the seed does not: [grafana runtime-config password]"). With -base pointing at a copy that holds only the searxng line, go run . -base <that copy> ../.. also exits 1.
The check seat's shallow clone works. In a git clone --depth 1 with the head fetched at depth 1, git merge-base HEAD origin/main fails. merge-check.sh then says "NOT COMPARED WITH THE TRUNK: no merge base with origin/main; checks/secrets/known is held to the list it was first written with", falls back to the seed, and refuses the same case: the test fails, and go run prints "checks/secrets/known gains "grafana runtime-config password" ..." and exits 1. A shallow clone therefore fails closed and says so, which is right.
NOT BLOCKING
In a shallow clone the comparison is with the seed, not the trunk. Once the trunk drops the searxng line (issue 610), a shallow-clone run would still allow that line back until the seed in the code is also emptied. Empty the seed in the same pull request that removes the line. TestALineTheTrunkDoesNotHoldIsRefused already holds known to the seed in every run, so a full clone has the same bound.
Verdict: approve at eb4280637906dfa117b16b71a3302a93e655be7f
Re-review of review 168's two blocking findings. Statuses at this head, read 2026-10-13 07:45, are all still pending: `mesh/merge-gate` pending ("building on ace since 07:43"), `mesh/delivery` pending ("checking"), `mesh/delivery-group` pending ("group fix/342-a-secret-is-never-a-setting checking: 0 of 2 member(s) ready"). `mesh/repo-check` has not been posted at this head yet. This approval holds only once `mesh/merge-gate` and `mesh/repo-check` are success at this head and have been read. The gate also tests the new `token` protection on grafana: if it still composes 4 of 4 machines, no live layer sets grafana `token`.
RESOLVED
1. grafana. `token` is now `protected` beside `password`. The client logs in with the vault's admin password first, ignores a `token` in the settings file and says so once by key (`the setting token is ignored: it comes from the vault`), and uses only `MESH_GRAFANA_TOKEN` from the environment after the password. I ran the new grafana tests locally: 5 of 5 pass at this head, and against main's grafana `client.ts` and `module.json` 2 of 5 fail. A controller built from mesh-controller main (a688dcf) accepts a protected key that is absent from the file's content: `MESH_GATE=<that controller> MESH_CHECK_MODULES=grafana,gitea,home-assistant,icecast,influxdb,mailu,nextcloud,baserow,letta,unifi,searxng sh merge-check.sh` exits 0, with "long-running resources without health: 67, as kept" and "secrets: ... (1 known, nested)".
2. The known list now only shrinks. I repeated my own failing case: I removed `password` from grafana's `protected`, added `grafana runtime-config password` to `known`, committed, and ran merge-check.sh. It now exits 1, refused by `TestALineTheTrunkDoesNotHoldIsRefused` ("known holds lines the seed does not: [grafana runtime-config password]"). With `-base` pointing at a copy that holds only the searxng line, `go run . -base <that copy> ../..` also exits 1.
3. The check seat's shallow clone works. In a `git clone --depth 1` with the head fetched at depth 1, `git merge-base HEAD origin/main` fails. merge-check.sh then says "NOT COMPARED WITH THE TRUNK: no merge base with origin/main; checks/secrets/known is held to the list it was first written with", falls back to the seed, and refuses the same case: the test fails, and `go run` prints "checks/secrets/known gains \"grafana runtime-config password\" ..." and exits 1. A shallow clone therefore fails closed and says so, which is right.
NOT BLOCKING
4. In a shallow clone the comparison is with the seed, not the trunk. Once the trunk drops the searxng line (issue 610), a shallow-clone run would still allow that line back until the seed in the code is also emptied. Empty the seed in the same pull request that removes the line. `TestALineTheTrunkDoesNotHoldIsRefused` already holds `known` to the seed in every run, so a full clone has the same bound.
Merge check at eb428063
Judged: head eb4280637906 merged into trunk 53e51864969f; read beside it: mesh-controller cd6757f1e3c8, mesh-controller-main a688dcff394b, mesh-host 7a41e120635b, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi)
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; its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
Delivery plan — builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi → ace, novox; no bus step; 1 wait(s) for a person
novox: receives gitea, nextcloud; waits for a person: mailu
baserow keeps data (ADR 0233): what it holds is backed up before it moves
gitea provides npm-package-registry, git: its consumers are sent again after it
gitea keeps data (ADR 0233): what it holds is backed up before it moves
grafana keeps data (ADR 0233): what it holds is backed up before it moves
home-assistant keeps data (ADR 0233): what it holds is backed up before it moves
icecast keeps data (ADR 0233): what it holds is backed up before it moves
influxdb provides influxdb-api: its consumers are sent again after it
influxdb keeps data (ADR 0233): what it holds is backed up before it moves
mailu waits for a person: its policy records (module)
mailu provides smtp: its consumers are sent again after it
mailu keeps data (ADR 0233): what it holds is backed up before it moves
nextcloud keeps data (ADR 0233): what it holds is backed up before it moves
unifi keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before baserow, grafana, home-assistant, icecast, influxdb, letta, unifi is judged on its first machine
read by no module's build: checks/secrets/go.mod, checks/secrets/known, checks/secrets/main.go, checks/secrets/main_test.go, merge-check.sh
Run by the build seat on ace as build-1791870217138535312 (builds --log build-1791870217138535312).
✔ no rule requires nothing; a rule that cannot be read says nothing, never yes (0.295495ms)
✔ a commit is contained in another when the compare finds nothing on it the other lacks (0.921943ms)
✔ a note line is appended once, as the forge's user, in its own repository (7.948628ms)
✔ a note that cannot be written is said, never read as written (0.349347ms)
✔ one view per pull request, found by its marker; only the mesh's statuses (0.504519ms)
✔ a build machine taking the check is said on the head: building on which machine, since the local time (18.236476ms)
✔ the verdict heard while a 'building' write is in flight lands last and stays (8.166148ms)
[gitea] novox/hq@a6b08de8: the verdict of build-1791680941000000000 is an older run's than the check now pending; not written
✔ the verdict written while a start's read of the forge is in flight stays the newest status (0.9576ms)
✔ the verdict heard while a 'building' write still waits its turn: the 'building' is never written (0.538262ms)
✔ a start heard after the verdict writes nothing (0.773224ms)
✔ a start from before the check now pending (a run before a recheck, the stream's history) writes nothing (0.709764ms)
✔ an old run's verdict redelivered after a recheck's pending is not written over it (0.849647ms)
[gitea] novox/hq@a6b08de8: the verdict of build-1791680941000000000 is an older run's than the check the forge holds pending; not written
[gitea] novox/hq@a6b08de8: the verdict of build-1791680941000000000 is an older run's than the check now pending; not written
✔ an old run's verdict in flight when the recheck comes lands before the recheck's pending (1.60685ms)
✔ a rechecked head says where its new run is: building on which machine (0.634692ms)
✔ the recheck's own verdict is written, even asked a little before the pending by another machine's clock (0.548731ms)
✔ after a restart, an old run's verdict is weighed against the check the forge's history holds open (0.458742ms)
✔ a recheck the forge does not take fails its caller, and the head is as it was (0.412846ms)
✔ the status tool writes the merge check's pending through the writer, and the mesh's other statuses as they were (4.527189ms)
✔ when a check was asked is read from its id; the opening of the check now pending from the gate's history (0.182944ms)
✔ a head this process did not announce (it restarted) is said only while the forge holds its gate pending (0.276519ms)
✔ a repository as the mesh clones it is told from another (0.221858ms)
✔ gitea_api refuses the forge's administration, a branch's protection and the merge check's status, and sends nothing (6.418472ms)
✔ gitea_api refuses a file written to the default branch or one a rule guards, and writes one on a branch of its own (51.657169ms)
✔ a protection rule covers a branch as the forge decides it: classes, alternatives, a plain name in any case (issue 539) (20.750611ms)
✔ gitea_api still writes what an author writes (7.683562ms)
✔ gitea_branch_protection_set only strengthens (14.977098ms)
✔ gitea_merge_pull_request merges only a head whose required statuses all succeeded, and only that head (45.432139ms)
✔ gitea_merge_pull_request refuses a verdict judged against an earlier trunk, and asks the check once (151.501215ms)
✔ gitea_merge_pull_request merges only the head it is named, and refuses one that is not the head (14.633003ms)
✔ gitea_merge_pull_request is loud when the forge merged a head other than the one it was handed (17.181212ms)
✔ gitea_merge_pull_request says the merge happened when reading back what landed fails (27.700874ms)
✔ gitea_merge_pull_request reads back a fast-forward, and says it cannot read back a squash or a rebase (62.881123ms)
✔ gitea_delete_repo deletes only the tools' own user's repository, unguarded, untouched for 30 days, named in full (22.826884ms)
✔ gitea_api sends the path it judged, and refuses moving a pull request's base (4.578765ms)
✔ gitea_merge_pull_request merges only into a branch whose protection requires the checks (7.622908ms)
✔ a rule named for a branch is not made weaker than the glob rule that guards it now (16.070143ms)
✔ a repository owner or name is never another path (0.619955ms)
✔ the judges, without a forge (0.484711ms)
✔ gitea_api never acts as another user: sudo, in any case, and any query key it does not send are refused (1.728479ms)
✔ gitea_api never approves a pull request nor requests changes: those are the operator's (2.424247ms)
✔ no tool's caller text becomes another path or another user: `..` and `?sudo=` are refused before the forge (3.917693ms)
✔ every page of a pull request's files is read, though the forge caps a page at fifty (26.901591ms)
✔ a pass asks only the repositories that moved since the last look, every one before the first (0.396104ms)
✔ the directories above a merge's files that hold a module at the commit are said, the root never (12.281263ms)
✔ a lookup the forge refuses is thrown, not read as no module (3.756761ms)
✔ a renamed file is said under its old name too, as gone (novox/hq ADR 0267) (5.257422ms)
✔ a new head beyond the newest 20 open pull requests is announced, exactly once (55.665713ms)
✔ a pass pages until it reaches back past its floor, and no further (30.456572ms)
✔ the statuses asked for replace the rule's, and an empty list requires none (6.248305ms)
✔ a new rule refuses direct pushes unless asked; an administrator's override only when said (0.261091ms)
✔ each open pull request's new head is announced once, and nothing closed or merged (9.9701ms)
✔ a verdict is the head commit's status; an error is the forge's error, never a success (1.159329ms)
✔ each layer is its own status: the gate with the modules it judged, the repository's own check beside it (0.713461ms)
✔ no verdict sets warning on a required check; only a person's decision fails (1.040205ms)
✔ a commit status is set on the commit, under the merge check's context (40.533662ms)
✔ a change plan is the gate's result: said on the status and, when it builds something, as a comment (0.715445ms)
✔ a head announced is pending in the build queue, since the local time it was announced (0.327776ms)
✔ the vault's token is used, a setting of it is ignored and said once, without its value (6.909024ms)
✔ first start: mints with the admin account, keeps the token at 0600, asks for the tools' scopes only (34.385087ms)
✔ second start: reuses the kept token, mints nothing (19.243106ms)
✔ the forge rejects the kept token (its data was restored): minted afresh, once, and the call goes through (23.304682ms)
✔ a token kept with write:admin by an earlier build is minted again without it at the next start, and the forge drops the old one (22.914618ms)
✔ a kept token lacking a scope the tools need: the forge refuses the call for the scope, the token is re-minted with the whole list, and the call goes through (20.098104ms)
✔ the kept file is gone but the forge still holds a token by that name: replaced, not refused (16.928998ms)
✔ concurrent first calls share one mint (12.401939ms)
✔ the admin account is refused: said plainly, nothing kept, and the next call fails the same way rather than crashing (13.547252ms)
✔ one process shares one source per kept file — the watcher and the tools never renew against each other (1.086733ms)
✔ a second process finds the token the first renewed, and reuses it instead of minting over it (21.676191ms)
✔ a configured token wins, and is reported rather than minted over when the forge rejects it (2.448482ms)
✔ nothing to mint with and no token: the client says what is missing (0.190338ms)
[gitea] no tools — no Gitea token, and nothing to mint one with — set MESH_GITEA_TOKEN, or MESH_GITEA_ADMIN_USER, MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-xwGTGQ/state/token
✔ the tools register once there is a way to a token, and the first call mints it (8.361705ms)
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-XFsNPU/state/token
✔ a pull request can be closed, read and cleaned up through the tools (19.979883ms)
[gitea] novox/mesh-host's main moved from a2c2c35bbbbb to d28ca95ccccc: 25 open pull request(s) into it put out of date, nothing run (novox/hq ADR 0323)
[gitea] novox/mesh-host's main moved from d28ca95ccccc to ffffffffffff: 0 open pull request(s) into it put out of date, nothing run (novox/hq ADR 0323)
[gitea] novox/mesh-host: whether a trunk moved could not be weighed, so it is weighed again on the next pass — the forge is away
[gitea] novox/mesh-host's main moved from a2c2c35bbbbb to d28ca95ccccc: 0 open pull request(s) into it put out of date, nothing run (novox/hq ADR 0323)
✔ both statuses say the head and the trunk commit the verdict judged, first, whatever the summary's length (1.725533ms)
✔ a trunk's move puts an open head's merge check out of date, naming both commits, unless a check is on its way (1.148488ms)
✔ a push to a trunk puts every open pull request into it out of date — twenty-five of them — and none into another (4.960814ms)
✔ a trunk that cannot be read is weighed again on the next pass, never recorded half (0.543682ms)
✔ the trunk's move takes its turn after a verdict in flight, and weighs what the forge holds then (0.517794ms)
✔ a verdict judged against a trunk newer than the pass's first read is not put out of date (0.300696ms)
✔ a verdict that landed out of date keeps its account on the pull request (0.281038ms)
ℹ tests 85
ℹ suites 0
ℹ pass 85
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 731.734006
type-checking modules/grafana (TypeScript)
testing modules/grafana (TypeScript)
✔ influxdb-api is optional, and route and oidc-client stay hard (0.763795ms)
✔ every file naming influxdb-api is delivered only while it is bound (0.297639ms)
✔ the container mounts the data source's directories, never a file that may be absent (0.209384ms)
✔ the vault's admin password is used, a token setting is ignored and said once, without its value (2.054803ms)
✔ the runtime config keeps password and token protected (0.426943ms)
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 75.726288
type-checking modules/home-assistant (TypeScript)
testing modules/home-assistant (TypeScript)
✔ the Servarr APIs and mqtt-topic are optional, and route stays hard (1.578237ms)
✔ mqtt: the broker is asked first; a login it does not take is never written (1.633129ms)
✔ mqtt: ace's entry (127.0.0.1, luffy) is moved to the bound broker and login, every other setting kept (2.179746ms)
✔ mqtt: a new password alone is written (the digest tells) (0.323398ms)
✔ mqtt: Home Assistant's own connection test refusing saves nothing and fails loudly (0.309522ms)
✔ mqtt: a fresh Home Assistant gets an entry; a grant without the discovery topics is warned about (0.422714ms)
✔ mqtt: two entries, or a binding without a port, are refused rather than guessed (0.229621ms)
✔ servarr: the mesh's minted key is never written; the remedy names the accept (0.67573ms)
✔ servarr: ace's entry at 127.0.0.1 reaches the same Sonarr the mesh bound at ace.internal — left, and said (0.402576ms)
✔ servarr: an entry already at the bound URL is unchanged (0.383881ms)
✔ servarr: a working entry that reaches a different app is refused, and nothing is removed (0.45237ms)
✔ servarr: no entry — one is made at the bound URL through the user flow (0.299122ms)
✔ servarr: a reauth Home Assistant started is finished with the bound URL and key (0.233779ms)
✔ form values: suggested first, then default, sections nested (0.125195ms)
✔ the vault's token is used, a setting of it is ignored and said once, without its value (1.983357ms)
ℹ tests 15
ℹ suites 0
ℹ pass 15
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 104.224571
type-checking modules/icecast (TypeScript)
testing modules/icecast (TypeScript)
✔ the vault's password is used, a setting of it is ignored and said once, without its value (1.725623ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 77.535219
type-checking modules/influxdb (TypeScript)
testing modules/influxdb (TypeScript)
✔ what a contribution may ask for, and what is refused (1.759978ms)
✔ a reader is given one authorization, reading every bucket of the org, under the mesh's password (39.070284ms)
✔ a writer is allowed its own buckets only, and a missing one is made — never deleted (22.430909ms)
✔ applying the same grant again writes nothing (13.972933ms)
✔ a rotated password is set in place; a changed access remakes only the mesh's own (60.385325ms)
✔ an authorization disabled, re-passworded or deleted behind the mesh's back is not held, and is made whole (49.199052ms)
✔ holds only reads, and a bucket gone missing is not held rather than made (15.047342ms)
✔ an authorization of the same name the mesh did not make is refused, and left exactly as it was (11.0826ms)
✔ the predecessor's own v1 users and tokens are never touched (12.977841ms)
✔ an org the instance does not have, or a non-mesh name, makes nothing (1.691309ms)
✔ a withdrawn consumer's authorization is removed, and an absent one is not an error (12.794517ms)
✔ the vault's token is used, a setting of it is ignored and said once, without its value (1.882718ms)
ℹ tests 12
ℹ suites 0
ℹ pass 12
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 352.849689
type-checking modules/letta (TypeScript)
NOT TESTED: modules/letta is TypeScript and has no tests of its own
type-checking modules/mailu (TypeScript)
testing modules/mailu (TypeScript)
✔ the vault's apiKey is used, a setting of it is ignored and said once, without its value (5.208169ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 82.068751
type-checking modules/nextcloud (TypeScript)
testing modules/nextcloud (TypeScript)
✔ the vault's password is used, a setting of it is ignored and said once, without its value (1.587103ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 72.619902
type-checking modules/unifi (TypeScript)
NOT TESTED: modules/unifi is TypeScript and has no tests of its own
**Merge check** at `eb428063`
Judged: head `eb4280637906` merged into trunk `53e51864969f`; read beside it: mesh-controller `cd6757f1e3c8`, mesh-controller-main `a688dcff394b`, mesh-host `7a41e120635b`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi)
- `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; its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
**Delivery plan** — builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi → ace, novox; no bus step; 1 wait(s) for a person
- tier 0: baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi
- ace: receives baserow, grafana, home-assistant, icecast, influxdb, letta, unifi
- novox: receives gitea, nextcloud; waits for a person: mailu
- baserow keeps data (ADR 0233): what it holds is backed up before it moves
- gitea provides npm-package-registry, git: its consumers are sent again after it
- gitea keeps data (ADR 0233): what it holds is backed up before it moves
- grafana keeps data (ADR 0233): what it holds is backed up before it moves
- home-assistant keeps data (ADR 0233): what it holds is backed up before it moves
- icecast keeps data (ADR 0233): what it holds is backed up before it moves
- influxdb provides influxdb-api: its consumers are sent again after it
- influxdb keeps data (ADR 0233): what it holds is backed up before it moves
- mailu waits for a person: its policy records (module)
- mailu provides smtp: its consumers are sent again after it
- mailu keeps data (ADR 0233): what it holds is backed up before it moves
- nextcloud keeps data (ADR 0233): what it holds is backed up before it moves
- unifi keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before baserow, grafana, home-assistant, icecast, influxdb, letta, unifi is judged on its first machine
- read by no module's build: checks/secrets/go.mod, checks/secrets/known, checks/secrets/main.go, checks/secrets/main_test.go, merge-check.sh
Run by the build seat on ace as `build-1791870217138535312` (`builds --log build-1791870217138535312`).
```
✔ no rule requires nothing; a rule that cannot be read says nothing, never yes (0.295495ms)
✔ a commit is contained in another when the compare finds nothing on it the other lacks (0.921943ms)
✔ a note line is appended once, as the forge's user, in its own repository (7.948628ms)
✔ a note that cannot be written is said, never read as written (0.349347ms)
✔ one view per pull request, found by its marker; only the mesh's statuses (0.504519ms)
✔ a build machine taking the check is said on the head: building on which machine, since the local time (18.236476ms)
✔ the verdict heard while a 'building' write is in flight lands last and stays (8.166148ms)
[gitea] novox/hq@a6b08de8: the verdict of build-1791680941000000000 is an older run's than the check now pending; not written
✔ the verdict written while a start's read of the forge is in flight stays the newest status (0.9576ms)
✔ the verdict heard while a 'building' write still waits its turn: the 'building' is never written (0.538262ms)
✔ a start heard after the verdict writes nothing (0.773224ms)
✔ a start from before the check now pending (a run before a recheck, the stream's history) writes nothing (0.709764ms)
✔ an old run's verdict redelivered after a recheck's pending is not written over it (0.849647ms)
[gitea] novox/hq@a6b08de8: the verdict of build-1791680941000000000 is an older run's than the check the forge holds pending; not written
[gitea] novox/hq@a6b08de8: the verdict of build-1791680941000000000 is an older run's than the check now pending; not written
✔ an old run's verdict in flight when the recheck comes lands before the recheck's pending (1.60685ms)
✔ a rechecked head says where its new run is: building on which machine (0.634692ms)
✔ the recheck's own verdict is written, even asked a little before the pending by another machine's clock (0.548731ms)
✔ after a restart, an old run's verdict is weighed against the check the forge's history holds open (0.458742ms)
✔ a recheck the forge does not take fails its caller, and the head is as it was (0.412846ms)
✔ the status tool writes the merge check's pending through the writer, and the mesh's other statuses as they were (4.527189ms)
✔ when a check was asked is read from its id; the opening of the check now pending from the gate's history (0.182944ms)
✔ a head this process did not announce (it restarted) is said only while the forge holds its gate pending (0.276519ms)
✔ a repository as the mesh clones it is told from another (0.221858ms)
✔ gitea_api refuses the forge's administration, a branch's protection and the merge check's status, and sends nothing (6.418472ms)
✔ gitea_api refuses a file written to the default branch or one a rule guards, and writes one on a branch of its own (51.657169ms)
✔ a protection rule covers a branch as the forge decides it: classes, alternatives, a plain name in any case (issue 539) (20.750611ms)
✔ gitea_api still writes what an author writes (7.683562ms)
✔ gitea_branch_protection_set only strengthens (14.977098ms)
✔ gitea_merge_pull_request merges only a head whose required statuses all succeeded, and only that head (45.432139ms)
✔ gitea_merge_pull_request refuses a verdict judged against an earlier trunk, and asks the check once (151.501215ms)
✔ gitea_merge_pull_request merges only the head it is named, and refuses one that is not the head (14.633003ms)
✔ gitea_merge_pull_request is loud when the forge merged a head other than the one it was handed (17.181212ms)
✔ gitea_merge_pull_request says the merge happened when reading back what landed fails (27.700874ms)
✔ gitea_merge_pull_request reads back a fast-forward, and says it cannot read back a squash or a rebase (62.881123ms)
✔ gitea_delete_repo deletes only the tools' own user's repository, unguarded, untouched for 30 days, named in full (22.826884ms)
✔ gitea_api sends the path it judged, and refuses moving a pull request's base (4.578765ms)
✔ gitea_merge_pull_request merges only into a branch whose protection requires the checks (7.622908ms)
✔ a rule named for a branch is not made weaker than the glob rule that guards it now (16.070143ms)
✔ a repository owner or name is never another path (0.619955ms)
✔ the judges, without a forge (0.484711ms)
✔ gitea_api never acts as another user: sudo, in any case, and any query key it does not send are refused (1.728479ms)
✔ gitea_api never approves a pull request nor requests changes: those are the operator's (2.424247ms)
✔ no tool's caller text becomes another path or another user: `..` and `?sudo=` are refused before the forge (3.917693ms)
✔ every page of a pull request's files is read, though the forge caps a page at fifty (26.901591ms)
✔ a pass asks only the repositories that moved since the last look, every one before the first (0.396104ms)
✔ the directories above a merge's files that hold a module at the commit are said, the root never (12.281263ms)
✔ a lookup the forge refuses is thrown, not read as no module (3.756761ms)
✔ a renamed file is said under its old name too, as gone (novox/hq ADR 0267) (5.257422ms)
✔ a new head beyond the newest 20 open pull requests is announced, exactly once (55.665713ms)
✔ a pass pages until it reaches back past its floor, and no further (30.456572ms)
✔ the statuses asked for replace the rule's, and an empty list requires none (6.248305ms)
✔ a new rule refuses direct pushes unless asked; an administrator's override only when said (0.261091ms)
✔ each open pull request's new head is announced once, and nothing closed or merged (9.9701ms)
✔ a verdict is the head commit's status; an error is the forge's error, never a success (1.159329ms)
✔ each layer is its own status: the gate with the modules it judged, the repository's own check beside it (0.713461ms)
✔ no verdict sets warning on a required check; only a person's decision fails (1.040205ms)
✔ a commit status is set on the commit, under the merge check's context (40.533662ms)
✔ a change plan is the gate's result: said on the status and, when it builds something, as a comment (0.715445ms)
✔ a head announced is pending in the build queue, since the local time it was announced (0.327776ms)
✔ the vault's token is used, a setting of it is ignored and said once, without its value (6.909024ms)
✔ first start: mints with the admin account, keeps the token at 0600, asks for the tools' scopes only (34.385087ms)
✔ second start: reuses the kept token, mints nothing (19.243106ms)
✔ the forge rejects the kept token (its data was restored): minted afresh, once, and the call goes through (23.304682ms)
✔ a token kept with write:admin by an earlier build is minted again without it at the next start, and the forge drops the old one (22.914618ms)
✔ a kept token lacking a scope the tools need: the forge refuses the call for the scope, the token is re-minted with the whole list, and the call goes through (20.098104ms)
✔ the kept file is gone but the forge still holds a token by that name: replaced, not refused (16.928998ms)
✔ concurrent first calls share one mint (12.401939ms)
✔ the admin account is refused: said plainly, nothing kept, and the next call fails the same way rather than crashing (13.547252ms)
✔ one process shares one source per kept file — the watcher and the tools never renew against each other (1.086733ms)
✔ a second process finds the token the first renewed, and reuses it instead of minting over it (21.676191ms)
✔ a configured token wins, and is reported rather than minted over when the forge rejects it (2.448482ms)
✔ nothing to mint with and no token: the client says what is missing (0.190338ms)
[gitea] no tools — no Gitea token, and nothing to mint one with — set MESH_GITEA_TOKEN, or MESH_GITEA_ADMIN_USER, MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-xwGTGQ/state/token
✔ the tools register once there is a way to a token, and the first call mints it (8.361705ms)
[gitea] no token kept — minting one
[gitea] the forge already holds a token named "mesh-tools" — replacing it
[gitea] minted a token for "mesh-admin" (write:repository, write:issue, read:user), kept at /tmp/gitea-XFsNPU/state/token
✔ a pull request can be closed, read and cleaned up through the tools (19.979883ms)
[gitea] novox/mesh-host's main moved from a2c2c35bbbbb to d28ca95ccccc: 25 open pull request(s) into it put out of date, nothing run (novox/hq ADR 0323)
[gitea] novox/mesh-host's main moved from d28ca95ccccc to ffffffffffff: 0 open pull request(s) into it put out of date, nothing run (novox/hq ADR 0323)
[gitea] novox/mesh-host: whether a trunk moved could not be weighed, so it is weighed again on the next pass — the forge is away
[gitea] novox/mesh-host's main moved from a2c2c35bbbbb to d28ca95ccccc: 0 open pull request(s) into it put out of date, nothing run (novox/hq ADR 0323)
✔ both statuses say the head and the trunk commit the verdict judged, first, whatever the summary's length (1.725533ms)
✔ a trunk's move puts an open head's merge check out of date, naming both commits, unless a check is on its way (1.148488ms)
✔ a push to a trunk puts every open pull request into it out of date — twenty-five of them — and none into another (4.960814ms)
✔ a trunk that cannot be read is weighed again on the next pass, never recorded half (0.543682ms)
✔ the trunk's move takes its turn after a verdict in flight, and weighs what the forge holds then (0.517794ms)
✔ a verdict judged against a trunk newer than the pass's first read is not put out of date (0.300696ms)
✔ a verdict that landed out of date keeps its account on the pull request (0.281038ms)
ℹ tests 85
ℹ suites 0
ℹ pass 85
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 731.734006
type-checking modules/grafana (TypeScript)
testing modules/grafana (TypeScript)
✔ influxdb-api is optional, and route and oidc-client stay hard (0.763795ms)
✔ every file naming influxdb-api is delivered only while it is bound (0.297639ms)
✔ the container mounts the data source's directories, never a file that may be absent (0.209384ms)
✔ the vault's admin password is used, a token setting is ignored and said once, without its value (2.054803ms)
✔ the runtime config keeps password and token protected (0.426943ms)
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 75.726288
type-checking modules/home-assistant (TypeScript)
testing modules/home-assistant (TypeScript)
✔ the Servarr APIs and mqtt-topic are optional, and route stays hard (1.578237ms)
✔ mqtt: the broker is asked first; a login it does not take is never written (1.633129ms)
✔ mqtt: ace's entry (127.0.0.1, luffy) is moved to the bound broker and login, every other setting kept (2.179746ms)
✔ mqtt: a new password alone is written (the digest tells) (0.323398ms)
✔ mqtt: Home Assistant's own connection test refusing saves nothing and fails loudly (0.309522ms)
✔ mqtt: a fresh Home Assistant gets an entry; a grant without the discovery topics is warned about (0.422714ms)
✔ mqtt: two entries, or a binding without a port, are refused rather than guessed (0.229621ms)
✔ servarr: the mesh's minted key is never written; the remedy names the accept (0.67573ms)
✔ servarr: ace's entry at 127.0.0.1 reaches the same Sonarr the mesh bound at ace.internal — left, and said (0.402576ms)
✔ servarr: an entry already at the bound URL is unchanged (0.383881ms)
✔ servarr: a working entry that reaches a different app is refused, and nothing is removed (0.45237ms)
✔ servarr: no entry — one is made at the bound URL through the user flow (0.299122ms)
✔ servarr: a reauth Home Assistant started is finished with the bound URL and key (0.233779ms)
✔ form values: suggested first, then default, sections nested (0.125195ms)
✔ the vault's token is used, a setting of it is ignored and said once, without its value (1.983357ms)
ℹ tests 15
ℹ suites 0
ℹ pass 15
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 104.224571
type-checking modules/icecast (TypeScript)
testing modules/icecast (TypeScript)
✔ the vault's password is used, a setting of it is ignored and said once, without its value (1.725623ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 77.535219
type-checking modules/influxdb (TypeScript)
testing modules/influxdb (TypeScript)
✔ what a contribution may ask for, and what is refused (1.759978ms)
✔ a reader is given one authorization, reading every bucket of the org, under the mesh's password (39.070284ms)
✔ a writer is allowed its own buckets only, and a missing one is made — never deleted (22.430909ms)
✔ applying the same grant again writes nothing (13.972933ms)
✔ a rotated password is set in place; a changed access remakes only the mesh's own (60.385325ms)
✔ an authorization disabled, re-passworded or deleted behind the mesh's back is not held, and is made whole (49.199052ms)
✔ holds only reads, and a bucket gone missing is not held rather than made (15.047342ms)
✔ an authorization of the same name the mesh did not make is refused, and left exactly as it was (11.0826ms)
✔ the predecessor's own v1 users and tokens are never touched (12.977841ms)
✔ an org the instance does not have, or a non-mesh name, makes nothing (1.691309ms)
✔ a withdrawn consumer's authorization is removed, and an absent one is not an error (12.794517ms)
✔ the vault's token is used, a setting of it is ignored and said once, without its value (1.882718ms)
ℹ tests 12
ℹ suites 0
ℹ pass 12
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 352.849689
type-checking modules/letta (TypeScript)
NOT TESTED: modules/letta is TypeScript and has no tests of its own
type-checking modules/mailu (TypeScript)
testing modules/mailu (TypeScript)
✔ the vault's apiKey is used, a setting of it is ignored and said once, without its value (5.208169ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 82.068751
type-checking modules/nextcloud (TypeScript)
testing modules/nextcloud (TypeScript)
✔ the vault's password is used, a setting of it is ignored and said once, without its value (1.587103ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 72.619902
type-checking modules/unifi (TypeScript)
NOT TESTED: modules/unifi is TypeScript and has no tests of its own
```
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes issue 342 (a password is taken as a setting, and outranks the vault's), in this repository. Decision: the issue's 2026-10-11 decision under the operator's rule "a password is not a setting" (shape 1 refused by the controller, shape 2 ignored by the client and said once), with ADR 0326 (
protected/takes-settingson a mergeable file).What
baserow,grafana,letta,unifikeeppasswordin theirruntime-configasprotected. The controller already refuses a layer that sets a protected key, naming the layer and the key, never the value:the mesh's layer sets "password" on runtime-config, and that module keeps "password" for itself — it is not settable(checked against mesh-controller main'sApplySettingswith each manifest).gitea(token),home-assistant(token),icecast(password),influxdb(token),mailu(apiKey) andnextcloud(password) clients no longer read the secret from the settings file. A value there is ignored and said once on stderr:the setting <key> is ignored: it comes from the vault(key only, never the value). icecast now reads its admin password from the vault's file (MESH_ICECAST_ADMIN_PASSWORD_FILE=${dir:state}/admin.secret, useradminas its config says); before, it had no source but a setting.checks/secrets(merge-check.sh step 6): a mergeable file whose key holds${secret:…}must keep that keyprotected, or leave it out of itstakes-settings.checks/secrets/knownlists the one nested case the controller cannot protect alone yet,searxng settings server.secret_key(serveris a key the file takes for the operator's other values, and ADR 0326 §4 refuses a key both taken and protected); a stale line is refused, so the list only shrinks.noderedwas listed in the issue and is no longer affected: itsconfig.jsondoes not merge, so no setting reaches its token.Not in this PR
server.secret_key) needs the controller to refuse a nested key, or to refuse any key a mergeable file fills with${secret:…}on its own, as the decision states; that is mesh-controller's part.passwordshows there as a refusal by layer and key; a layer holding a shape-2 key shows in that module's log line after rollout.Tests (fail on main, pass here)
modules/{gitea,home-assistant,icecast,influxdb,mailu,nextcloud}/test/secret-setting.test.ts: the vault's value is used with a setting present, the line is said once over two builds, and never with the value. On main all six fail.checks/secrets: unit tests, plus the catalogue itself; on main it names baserow, grafana, letta and unifi.MESH_GATE=<controller from mesh-controller main 8f1aec0> MESH_CHECK_MODULES=gitea,home-assistant,icecast,influxdb,mailu,nextcloud,baserow,grafana,letta,unifi,searxng sh merge-check.shpasses (67 resources without health, as kept);merge-check-typescript.shfor the same modules passes (gitea 85, home-assistant 15, influxdb 12, icecast/mailu/nextcloud 1 each).Reviewers' checklist: acceptance (no module here takes a secret from a setting; a setting naming one is refused or ignored and said); tests fail without the fix; no value printed anywhere; manifests consistent (
protectedonly on mergeable files, none also intakes-settings); checks green.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.Delivery
novox/mesh-catalog@eb4280637906— delivered since 2026-10-13T05:59:21ZDelivery plan — builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi → ace, novox; no bus step; 1 wait(s) for a person
Group
fix/342-a-secret-is-never-a-setting, in order: novox/mesh-catalog@eb42806379 → novox/mesh-media-catalog@c06014fbf8Machines
Transitions
The commit's note under
refs/notes/mesh-plankeeps every transition:git log --notes=mesh-plan.Merge check at
fe2040c6Judged: head
fe2040c65ea2merged into trunk53e51864969f; read beside it: mesh-controllercd6757f1e3c8, mesh-controller-main8f1aec0832b1, mesh-host7a41e120635b, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi)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; its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)Delivery plan — builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi → ace, novox; no bus step; 1 wait(s) for a person
Run by the build seat on shanks as
build-1791869565559543520(builds --log build-1791869565559543520).Verdict: request changes at
fe2040c65eIndependent review for hq issue 342 (its 2026-10-11 decision) and ADR 0326. Statuses at this head, read 2026-10-13 07:40:
mesh/merge-gatesuccess ("headfe2040c65emerged into trunk53e5186496— pass: builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, n…", 4 of 4 machines compose),mesh/repo-checksuccess ("headfe2040c65emerged into trunk53e5186496— pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh)…"),mesh/deliverysuccess ("ready: it delivers once merged"),mesh/delivery-groupsuccess ("group fix/342-a-secret-is-never-a-setting ready: every member ready, and composed together they pass").BLOCKING
A setting can still replace grafana's credential.
modules/grafana/client.tsreadsconst token = cfg.token ?? env.MESH_GRAFANA_TOKENand, when it is set, authenticates with it as a Bearer token instead of the vault's admin password. grafana'sruntime-configmerges JSON, names notakes-settings, and so takes every key of the mesh-wide and node layers (ADR 0326 §3, today's form).tokenis not in the file's content and is not protected, so a layer settingtokenlands in config.json and outranks the vault. Protectingpasswordalone does not meet the acceptance test ("no module takes a password, token or API key from a setting"). Fix it as shape 2: ignorecfg.token(and keep readingcfg.passwordonly from the protected file) with the same said-once line, and add a test. The same reading applies to baserow, letta and unifi: their clients readcfg.passwordfirst, which is now protected, and no other secret key, so they are fine.checks/secrets/knowncan grow, although the PR and the check's comment say it only shrinks.Judgerefuses a stale line, but a new finding with a new line beside it passes. I checked this: removingprotectedfrom grafana'sruntime-configand addinggrafana runtime-config passwordtoknownmakesgo run . ../..print "no setting can replace a vault secret in a mergeable file (2 known, nested)" and exit 0. merge-check.sh judgeschecks/paths/knownagainst the base branch's copy (base_lists), but it does not do that forchecks/secrets/known. Judge it againstorigin/${MESH_CHECK_BASE:-main}:checks/secrets/knownthe same way, refusing a line the base does not have, and add a test for it.NOT BLOCKING
The tests fail on main and pass at this head, which I checked locally. With main's
client.ts,module.jsonandpackage.jsonand the head's tests, merge-check-typescript.sh fails one test in each of gitea, home-assistant, icecast, influxdb, mailu and nextcloud, andchecks/secretsnames baserow, grafana, letta and unifi. At the head,MESH_GATE=<mesh-controller main 8f1aec0> MESH_CHECK_MODULES=gitea,home-assistant,icecast,influxdb,mailu,nextcloud,baserow,grafana,letta,unifi,searxng sh merge-check.shexits 0 ("long-running resources without health: 67, as kept"), and merge-check-typescript.sh for the same modules exits 0.The ignored line names the key only. Every new
ignoreSecretSettingprintsthe setting <key> is ignored: it comes from the vaultand never the value, and the tests assert that the value is absent.No lockout follows from the manifests. Reasoning from the manifests: icecast's admin password is a required provision secret already rendered into icecast.xml, and nextcloud uses the same tool-side path (
${dir:state}/admin.secret). gitea falls back to its minted token, influxdb's and nextcloud's own secrets are minted and given to the application, mailu'sapi-tokenis the provision secret given to mailu asAPI_TOKEN, and home-assistant's provisions already need the acceptedtokenfile. For the four protected files, the gate composing 4 of 4 machines "as it did without" shows that no live layer sets a now-protectedpasswordtoday. The decision asks the pull request to say how many layers and where, so state that count (0, by the gate) in the description. One caution: baserow'sadminand unifi'scontrollerare minted secrets that the manifests never give to Baserow or UniFi, whose accounts are made in their own UIs. Their tools work only if the operator accepted the real password into the vault. This PR does not make that worse, but it is worth one line in the description.Small points. home-assistant's error still says "set MESH_HOMEASSISTANT_TOKEN", while the real remedy is to accept the
tokenown secret. The secrets check does not see a client that reads a secret key absent from the file's content (finding 1); say that limit in the check's comment. searxng's nestedserver.secret_keystays open until the controller can protect a nested key. Name the follow-up issue for that, so theknownline has an owner.Review 168 addressed at head
eb42806379. (1) grafana:tokenis now protected besidepassword, and the client logs in with the vault's admin password first; atokenin the settings file is ignored and said once by key (onlyMESH_GRAFANA_TOKENfrom the environment is used, after the password).modules/grafana/test/secret-setting.test.tsfails atfe2040c6(2 of 5) and passes here. (2) checks/secrets/known only shrinks: merge-check.sh reads the trunk's copy at the merge base and passes it with-base, and the check refuses any line the trunk's copy does not hold. Until the trunk holds a copy, a seed in the code (the searxng line) stands for it. Tests:TestALineTheTrunkDoesNotHoldIsRefused,TestTheCheckRefusesALineAddedAgainstTheTrunk. I reproduced the review's case: unprotecting letta'spasswordand adding its line toknownfails merge-check.sh. The nested-key follow-up is issue 610.Verdict: approve at
eb42806379Re-review of review 168's two blocking findings. Statuses at this head, read 2026-10-13 07:45, are all still pending:
mesh/merge-gatepending ("building on ace since 07:43"),mesh/deliverypending ("checking"),mesh/delivery-grouppending ("group fix/342-a-secret-is-never-a-setting checking: 0 of 2 member(s) ready").mesh/repo-checkhas not been posted at this head yet. This approval holds only oncemesh/merge-gateandmesh/repo-checkare success at this head and have been read. The gate also tests the newtokenprotection on grafana: if it still composes 4 of 4 machines, no live layer sets grafanatoken.RESOLVED
grafana.
tokenis nowprotectedbesidepassword. The client logs in with the vault's admin password first, ignores atokenin the settings file and says so once by key (the setting token is ignored: it comes from the vault), and uses onlyMESH_GRAFANA_TOKENfrom the environment after the password. I ran the new grafana tests locally: 5 of 5 pass at this head, and against main's grafanaclient.tsandmodule.json2 of 5 fail. A controller built from mesh-controller main (a688dcf) accepts a protected key that is absent from the file's content:MESH_GATE=<that controller> MESH_CHECK_MODULES=grafana,gitea,home-assistant,icecast,influxdb,mailu,nextcloud,baserow,letta,unifi,searxng sh merge-check.shexits 0, with "long-running resources without health: 67, as kept" and "secrets: ... (1 known, nested)".The known list now only shrinks. I repeated my own failing case: I removed
passwordfrom grafana'sprotected, addedgrafana runtime-config passwordtoknown, committed, and ran merge-check.sh. It now exits 1, refused byTestALineTheTrunkDoesNotHoldIsRefused("known holds lines the seed does not: [grafana runtime-config password]"). With-basepointing at a copy that holds only the searxng line,go run . -base <that copy> ../..also exits 1.The check seat's shallow clone works. In a
git clone --depth 1with the head fetched at depth 1,git merge-base HEAD origin/mainfails. merge-check.sh then says "NOT COMPARED WITH THE TRUNK: no merge base with origin/main; checks/secrets/known is held to the list it was first written with", falls back to the seed, and refuses the same case: the test fails, andgo runprints "checks/secrets/known gains "grafana runtime-config password" ..." and exits 1. A shallow clone therefore fails closed and says so, which is right.NOT BLOCKING
TestALineTheTrunkDoesNotHoldIsRefusedalready holdsknownto the seed in every run, so a full clone has the same bound.Merge check at
eb428063Judged: head
eb4280637906merged into trunk53e51864969f; read beside it: mesh-controllercd6757f1e3c8, mesh-controller-maina688dcff394b, mesh-host7a41e120635b, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi)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; its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)Delivery plan — builds baserow, gitea, grafana, home-assistant, icecast, influxdb, letta, mailu, nextcloud, unifi → ace, novox; no bus step; 1 wait(s) for a person
Run by the build seat on ace as
build-1791870217138535312(builds --log build-1791870217138535312).