ADR 0283: a part waiting for the operator's secret or setting is said as waiting and passes the first-node gate (issue 386)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/386-a-wait-for-the-operator checking: 1 of 2 member(s) ready
mesh/delivery superseded: a newer head of the same pull request

A module whose only unhealthy part waited for the operator's secret failed
its gate, held every later walk and told the operator there was nothing to
do. The record adds a waits answer and a waiting state, checked by the
controller, read by the gate as waits for a person, and said as a
needs-operator condition naming the act; and secret families so each SMB
source's password is one hidden line at the desk prompt.
This commit is contained in:
jochen
2026-10-10 21:05:29 +02:00
parent 5f4ff3a80b
commit 242a6c548b
5 changed files with 250 additions and 14 deletions
@@ -111,6 +111,13 @@ Rejected:
vault and unsealed on the node as a file (ADR 0086). The module writes one credentials file per source
from it, readable by root only, which the mount unit names. No password is written in a
repository, a manifest or `/etc/fstab`.
> **The mechanism changed — 2026-10-10, by ADR 0283.** That a share's credential is an operator-delivered
> secret, issued outside the mesh and unsealed on the node as a file, stands. The one secret
> `smb-credentials` holding every login is replaced: the username is the setting `smb-users`, and each SMB
> source's password is its own one-line secret `smb-password-<source>`, a member of a secret family the
> mesh never makes a value for, so it can be typed at the desk prompt one source at a time
> ([ADR 0283](0283-a-part-that-waits-for-the-operators-secret-or-setting-is-said-as-waiting-passes-the-first-node-gate-and-names-the-act.md)).
8. **Health:** a share that should always be reachable and is not, for 5 minutes, raises one plain
condition, naming the share, the node and since when. It is the `mounts` module's health (ADR 0240),
judged by its tool `mounts_health`, whose finding is *share unreachable*; no new kind of condition is
@@ -0,0 +1,192 @@
---
topic: what runs on it
status: accepted
date: 2026-10-10
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0240-a-module-says-how-it-is-healthy-and-the-node-engine-judges-it.md
---
# 283. A part that waits for the operator's secret or setting is said as waiting, passes the first-node gate, and names the act
## Context
On 2026-10-10 the mounts module ([ADR 0263](0263-a-share-is-a-provision-a-node-mounts-it-on-demand-and-only-nodes-that-ask-get-it.md))
was given an occasional source on the workstation: the gaming device's SMB share, approved by the operator on
their phone at 15:06 through a settings proposal ([ADR 0277](0277-a-trusted-setting-is-proposed-through-a-verb-and-set-on-the-operators-warrant-and-a-modules-own-secret-is-asked-for-and-typed-at-the-desk.md)).
Its login was never given. What followed is issue 386, with issue 379 behind it:
- **The module said the wait, and the mesh read it as a fault.** Its health tool answered *not healthy*:
"the source … cannot be mounted: its login is not given". The node-engine has two answers from a tool
check, healthy or not (ADR 0240 rule 2), so it stated the resource `unhealthy`. The controller raised
`module.mounts.<machine>.unhealthy`, urgent after four hours, whose plain words said "Nothing for you to
do … It clears as soon as it runs again". Both halves were false: it waited for the operator, and nothing
but the operator would clear it.
- **The first-node gate failed a build for it.** A fix to mounts sat in its gate for ten minutes at 0 of 3
passes and was put back (`build.mounts.<machine>.rolled-back`), because the gate reads a module's stated
health and the resource was unhealthy. Every later walk waited behind it, since one walk runs at a time.
The fault was not the build's: any build would have said the same until the login was given.
- **The intended path could not give what the module wanted.** The module took one own secret,
`smb-credentials`, holding a `[source]` section with `username=` and `password=`. The desk prompt that
`secret-ask` opens (ADR 0277 §5) takes one hidden line. A later build of the module read a one-line form,
but a login typed blind as `[name] username=… password=…` is a line the operator gets wrong without seeing
it, and several sources would need several lines the prompt cannot take.
- **The mesh puts a value of its own in a secret nobody gave.** For an own secret not given, the controller
makes a random value and places it, even for one whose manifest says `"issued-by": "outside"`. That is how
the module came to read "line 1 is not key=value inside a [section]": the file held forty random
characters. ADR 0228 already says no value of the mesh's works in place of an outside party's; making one
anyway hides "not given" behind "given and wrong".
- **One precedent exists.** ADR 0254 made a wait for a person's new login a gate reading of its own,
*waits for a person*, which passes and is carried in the verdict, and a condition of its own,
`relogin-needed`, the operator's, never escalated by age. Issue 339 did the same for a directory used as
found. Both are recognised by the controller from what the node-engine says of a kind of resource. Neither
lets a module say it waits.
**Checked against GENESIS.** *Failure must be loud*, and a wait is not a failure: a gate that undoes a build
for the operator's missing step says something false, loudly, and a condition that says "nothing for you to
do" about the one thing only the operator can do is the quiet half of the same fault. *Evidence over
assertion*: a module saying it waits is an assertion, so the controller checks it against what it holds
before it excuses anything. *When something needs a decision, you are asked, with the context*: the act is
named exactly, once. Nothing here conflicts with GENESIS.
## Considered Options
**How the wait is said.**
1. *Keep it unhealthy, and fix only the words of the condition.* The gate still fails every build sent
while a secret is owed. Rejected.
2. *A reason prefix on an unhealthy resource, as `relogin needed` and `used as found` are.* Works with no
new state, but a module's tool says free text, and a prefix on free text is a word anyone can type.
Rejected for a module's own check.
3. **A third answer of a health tool, *waits*, carried as a state of its own, `waiting`, naming each thing
it waits for: an own secret or a setting of the module, by name, and what the operator gives, in
words.** Chosen. Structured, so the controller can check it; a state, so nothing that reads `unhealthy`
counts it as a fault.
**What the gate does with it.**
4. *Read it as waiting*, as a provider down is read: no pass, no failure. The walk then waits for the
operator, possibly for days, and every walk behind it with it. That is issue 386 without its rollback.
Rejected.
5. **The reading *waits for a person* of ADR 0254, carried in the verdict: a pass.** Chosen, under a check:
the controller excuses a wait only when what it names is the module's and is not given (decision 3).
**Whether only the move that brought the wait is excused**, as ADR 0254 narrowed its own.
6. *Yes.* A new login is owed because one build added a group, so a later build cannot have brought it. A
secret not given is owed by every build alike; excusing only the first would fail every fix sent while
the operator is away, which is the stall this record removes. Rejected.
7. **No: any build is excused while what it waits for is, on the record, not given.** Chosen. The cost is in
the consequences.
**How a module with several sources takes one hidden line per password.**
8. *One secret holding every login, typed in a set form.* Issue 386's own path. Rejected above.
9. *One fixed secret per module for the password, with the username as a setting.* One SMB source per
machine, and a second needs a change to the manifest. Rejected.
10. **An own secret may be declared as a family, `<prefix>-*`: one member per part the settings name, each
one line, given by its full name.** Chosen. The username is an ordinary setting, since it is not a
secret, and it travels through a settings proposal the operator approves on their phone.
## Decision
1. **A health tool answers one of three: healthy, unhealthy, or waits.** The answer keeps `healthy` and
`why`, and gains `state` (`healthy`, `unhealthy`, `waits`) and `waits`: a list, each naming exactly one
own secret (`secret`) or one setting (`setting`) of the module by name, the part that waits (`part`),
and what the operator gives, in words with no path, address or value (`what`). `waits` means nothing
else of the module is wrong. A module with a fault beside a wait answers `unhealthy` and says both.
An answer that says `waits` with no wait, or a wait naming neither or both, is read as unhealthy with
that reason. The type, its constructors and its reader are mesh-sdk's (`go/health`), so the module and
the node-engine read one shape; an answer without `state` is read from `healthy` as before.
2. **The node-engine states such a resource `waiting`**, a fifth state beside healthy, unhealthy,
starting, held and unknown (ADR 0240 §4), with its waits. One look that says it is enough, inside the
grace too: it is a statement, not a flake that a second look might not repeat. The next look that says
anything else replaces it.
3. **The controller checks a wait before it excuses it.** A wait for a secret is excused only when the
module's manifest declares that own secret (or the family it belongs to, decision 6) as
`"issued-by": "outside"`, and the store holds no value given for it on that machine. A wait for a
setting is excused only when the manifest declares that setting. A wait that fails the check is judged
as unhealthy, saying why ("says it waits for … , which was given at …").
4. **The first-node gate reads an excused wait as *waits for a person* (ADR 0254), a pass**, and the
verdict carries it: "… and it waits for the operator: <what> on <machine>". Nothing is put back for it.
Any build of the module is excused while the wait checks out; a module with anything else unhealthy is
judged as before.
5. **The wait is said to the operator as the module's condition `module.<module>.<machine>.needs-operator`**,
in place of `unhealthy`: a warning, the operator's, never escalated by age, raised on two statements in a
row and cleared on the first that no longer says it, as every module condition is. Its plain words
(ADR 0253) name the act and never say "nothing for you to do": for a secret, "Needs you: type <what> at
<machine>'s desk prompt. An agent opens the prompt for you when asked"; for a setting, "Needs you: approve
<setting> of <module> on <machine> when an agent proposes it". Its summary names the secret or setting and
the line (`nox secret ask <machine> <module> <secret>`), for whoever looks closer. A wait that fails the
check raises the module's `unhealthy` condition instead.
6. **An own secret may be a family.** A name ending in `-*` in `own-secrets` declares one: its path holds
exactly one `*`, and it must say `"issued-by": "outside"`. A member is the prefix followed by a name's
characters (`smb-password-games`), given by that full name through `secret accept` or `secret-ask`, and
placed at the path with `*` replaced by what follows the prefix. **The mesh never makes a member**: one
not given is no file, and the module reads its absence as not given. `module check` refuses a family
without `issued-by: outside`, or whose path does not hold exactly one `*`.
7. **The mounts module takes a source's login as a setting and a secret.** The username is the setting
`smb-users` (`name=username`, space-separated). The password is the family `smb-password-*`, one member
per SMB source, one hidden line each. A source whose username or password is not given is not mounted
and is said as waiting for that setting or secret, never as unhealthy. The `smb-credentials` secret and
its form are dropped: nothing was ever given for it.
## Consequences
- **Issue 386's walk passes.** A build of mounts sent while the password is owed passes its gate with the
wait carried, and later walks go on.
- **The operator reads one sentence that names the act**, "Needs you: type the password of the source …
at <machine>'s desk prompt", which clears once the password is given and the source is said healthy or
absent.
- **What the gate cannot tell.** While a wait checks out, a build that also breaks the waiting part itself
(a source whose mount would fail for a reason of its own once the password came) passes. That fault is
said after the password is given, as the module's own `unhealthy` condition, and a newer build or a
person's push is the way, as ADR 0254 already accepts for its wait.
- **A setting's wait is checked by name only.** The controller cannot read whether a free-form setting
covers a part. A module that says it waits for a setting it declares is believed; the needs-operator
condition says it in front of the operator, which is where a false one shows.
- **The state is new on the wire.** A controller older than this reads `waiting` as not healthy and raises
nothing, so the gate fails at the bound as before. A node-engine older than this reads `waits` as not
healthy, as before. Rollout order: mesh-sdk, then the controller, then the node-engine, then the module.
- **An outside secret not given stops being made only for a family.** A plain own secret marked
`issued-by: outside` is still made when not given; changing that touches every module that declares one,
and is tracked by issue 401.
- **The workstation's fstab line for the device** is adopted only once the source is mounted by the module
(issue 379).
> **The mechanism changed — 2026-10-10, by this record.** ADR 0263 rule 7 named one own secret,
> `smb-credentials`, holding every SMB login. Decision 7 above replaces it with the setting `smb-users` and
> the secret family `smb-password-*`. That a credential for a share is an operator-delivered secret stands.
## How it is checked
| Rule | Checked by |
|---|---|
| 1 Three answers | mesh-sdk `go/health` tests: each constructor's JSON; the reader on an answer with no state (healthy and not), on `waits` with no wait, a wait naming neither or both, a `what` with a path or an address — each read as unhealthy with its reason |
| 2 The state `waiting` | node-engine (mesh-host) `internal/liveness` tests: a tool check answering `waits` is stated `waiting` with its waits, in grace and after; the next healthy answer is `healthy`; an engine reading an answer without `state` judges as before |
| 3 The check | controller tests: a wait for an outside secret not given is excused; for one given, for one not declared, for one not `issued-by: outside`, and for a setting not declared, it is not, each with its reason |
| 4 The gate | controller tests: a module whose only resource not healthy is an excused wait passes with the wait in the verdict; the same with a second resource unhealthy is not yet; a later build is excused too |
| 5 The condition | controller tests: two statements of an excused wait raise `needs-operator`, a warning, resolver operator, its needs naming the desk prompt for a secret and the approval for a setting, never "nothing for you to do"; a statement without it clears it; a wait that fails the check raises `unhealthy` |
| 6 Families | controller tests: `module check` refuses each malformed family; a member is accepted and asked for at the desk by its full name, a name outside the family is refused; a member given is placed at its path; a member not given places nothing and composes |
| 7 mounts | the module's tests: a source without username waits for `smb-users`, without password waits for `smb-password-<source>`, with both is written; a password file is read as one line; health answers `waits` and not `unhealthy`; nothing logs or answers a password |
| live | on the workstation, before the password: `conditions` shows `module.mounts.<machine>.needs-operator` naming the desk prompt, no `unhealthy`, and the next mounts walk passes its gate. After `secret-ask` and the operator's typing: the condition clears, and the source mounts when the device is on |
## References
- Issue 386 (the gate), issue 379 (the workstation's two hand-written mounts).
- [ADR 0240](0240-a-module-says-how-it-is-healthy-and-the-node-engine-judges-it.md) (the states, the tool
check), [ADR 0254](0254-a-wait-for-a-persons-new-login-is-not-a-gates-failure-and-each-module-of-a-send-keeps-its-own-pass.md)
(*waits for a person*), [ADR 0253](0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md)
(plain words), [ADR 0277](0277-a-trusted-setting-is-proposed-through-a-verb-and-set-on-the-operators-warrant-and-a-modules-own-secret-is-asked-for-and-typed-at-the-desk.md)
(the proposal and the desk prompt), [ADR 0228](0228-a-value-given-by-hand-lives-only-until-its-modules-first-good-start.md)
(issued outside), [ADR 0263](0263-a-share-is-a-provision-a-node-mounts-it-on-demand-and-only-nodes-that-ask-get-it.md)
rule 7.
- [To-be 48](../03-DESIGN/01-to-be/48-a-module-says-how-it-is-healthy.md) §4 and §5, and
[to-be 52](../03-DESIGN/01-to-be/52-shares-and-mounts.md): the designs this amends.
- mesh-sdk, mesh-controller, mesh-host and mesh-catalog: the branches `feat/386-a-wait-for-the-operator`.