Six places asserted the old design as fact, and none of them mention the
change by name -- the class of doc breakage that is found by asking what
a diff made untrue, not by grepping for a feature:
- swarmctl/README.md and swarm-authelia-bridge/README.md both described
their own private canonical store. The bridge's "known limitation"
section described the seam as unsolved; it is what this fixes, so it
becomes what both writers must uphold instead.
- docs/swarm/{sso,ui,secrets}.md described a rendered artifact.
- The repo CLAUDE.md entry for swarmctl said the same.
- docs/tools/swarmctl-cli.md is regenerated (CI diffs it against the
clap tree), picking up the removed --store flag.
Operator-facing where it is read: the hand-editing consequence (values
survive a rewrite, comments do not) is stated in sso.md, where an
operator is being told to edit the file, rather than only in a module doc.
#3414 fixed the missing-email defect in swarmctl only, so every identity
created by the bridge landed in users.yml with no `email`. A relying
party that asks for the claim does not degrade, it fails -- grafana's
OIDC login is the measured case (#3393).
Two writers of one file disagreeing about a field one of them treats as
required is not a difference worth keeping, and with the file shared the
result also depended on which tool wrote last.
Mutation-checked: removing the fill turns the new test red and nothing
else.
`swarm agent create` failed with "no user store at
swarm-authelia-bridge-users.json but users.yml already holds users" on
any hive that had users. The guard was correct; what was wrong is that
two files both claimed to be canonical for one physical file.
The bridge kept a private users.json and rendered users.yml from it,
while swarmctl kept its own pair against the same users.yml. A writer
whose own JSON was absent could not tell "nothing here yet" from
"someone else's users", so it refused to write at all.
users.yml becomes the store: read before write, through serde_norway
rather than a hand-rolled emitter. Unknown top-level and per-user keys
round-trip through `extra`, or whichever process writes second would
silently delete what the first added. Validation stays on the write path
-- a bare to_string(&store) serialises perfectly and drops the "no
control character ever reaches this file" guarantee silently.
The seed constant goes with the guard: nothing writes a seed now, an
absent file is an empty store, and left as a pub constant it read as if
the seed dance were still load-bearing.
swarm-authelia-bridge answered one 401 for four causes, including its own
client credentials being rejected by authelia — telling a caller its token
was invalid when the fault was ours. That cost a real diagnostic round-trip,
because the only thing separating the two was a log line inside a container.
Introspection now returns a three-way Verdict, and one classify() maps it to
either admission, a 401 (the caller's credential) or a 503 (ours). Admission
is unchanged: Ok is reachable from exactly one variant.