Agents have authelia *users*; they had no machine identity at all, so an
agent could not authenticate to the swarm queue as anything. This mints
one `agent-<hive>` OIDC client per hive beside the existing
`hive-<hive>` one, teaches the auth-callout responder an agent arm, and
opens the queue's client port on the bridge so a container can reach it.
One client per HIVE, not per agent: agents are created at runtime, and a
per-agent client would make creating one a config change plus an
authelia reload. The cost is that agents on a hive are indistinguishable
to the broker, which is deliberate and tracked separately.
The agent grant is deny-by-default twice over. An agent id matches no
hive rule, so it gets a hive's status-key grant from neither; and with
no agent subject configured the responder returns no grant at all rather
than an empty publish list, which would be a denial wearing a grant's
shape. What an agent may publish is a deployment's decision, taken
through `--agent-publish-subject` the same way `--hive-publish-subject`
already works.
`Policy::new` now refuses two prefixes where one contains the other. The
arms are tried in order, so that overlap does not error at match time -
it silently hands one principal the other's grant.
Not shipped here, and neither is reachable without it: no subject is
configured for agents anywhere in nix, and nothing yet delivers
`agent-<hive>.secret` into an agent container. Both belong to the stream
that will be the first consumer.
The three cross-references this branch rewrote used `*Swarm SSO*`; prettier
emits `_Swarm SSO_`, so checks.formatting went red on an otherwise
content-only change.
Verified: nix fmt is now idempotent on this tree (0 changed); the three
scripts/check-*.sh lints exit 0 with the change staged.
`c1b7be11` inserted `### 3 · Secret store` and renumbered every heading below
it — SSO 3→4, UI 4→5, Matrix 5→6, Spawn 6→7, Host commands 7→8 — without
touching a single cross-reference. Five were left pointing one section short:
line 25 "see step 6" -> §6 Matrix, meant §7 Spawn sub-agents
line 30 "in step 6" -> §6 Matrix, meant §7 Spawn sub-agents
line 35 "see step 3" -> §3 Secret store, meant §4 Swarm SSO
line 217 "(step 3)" -> §3 Secret store, meant §4 Swarm SSO
line 246 "see step 3" -> §3 Secret store, meant §4 Swarm SSO
Both `step 3` targets are account-creation instructions and §3 is the secret
store, which is `only when deploy.bao` — so an operator without a store follows
the pointer into a section that does not apply and finds no `swarmctl` in it.
Renumbering to 4/7 would rot on the next insertion. These name the section
instead, the form `approvals.md:87` and `dashboard.md:906` already use. The
Matrix block's `# 5a.`–`# 5d.` comments lose their prefixes for the same
reason: they numbered themselves against that section's old position, and the
page's other code blocks carry unnumbered comments anyway.
Closes#4212.
Two unrelated changes landed correctly and left their prose behind, both
on operator-facing surfaces.
`hivectl agent <name> choom` gained a `hive-admin` path when hive-c0re
started shipping a polkit rule for `machinectl shell`. `choom.rs`
implements exactly that — `euid != 0 && !in_hive_admin_group()` — but
the `Choom` variant's clap doc comment still said "Requires root and a
running container", eight weeks on. That string is what
`choom --help` prints and what renders into the generated CLI
reference, so it is the sentence an operator actually reads, and it
tells a `hive-admin` member the command is not available to them.
`docs/turn-loop/mcp.md` carried the same staleness inside a
trust-boundary argument ("an operator (root) action"), where naming a
narrower reachable set than the real one is the wrong way to be wrong.
Dating it rather than asserting it: the acceptance landed 2026-09-07,
the doc string 2026-07-16.
Separately, `docs/tools/hivectl.md` states the agent-name hoist itself
("the name is hoisted onto the parent command, so none of the verbs
below repeat it") and its own example block obeys it, then twice writes
`set-limits sock --memory-max 8G` — the pre-hoist order. The generated
reference is unambiguous: `hivectl agent <NAME> <COMMAND>` versus
`hivectl agent set-limits [OPTIONS]`, with `quota set <SIZE>` nearby
showing that positionals do render when they exist. The costly one is a
complete command in backticks inside the paragraph explaining the
footgun it illustrates.
`docs/tools/hivectl-cli.md` is regenerated rather than hand-edited; the
diff against the committed copy is exactly one line.
Closes#4236.
mara's call on this PR was "list schedules not being scoped at all is a
bug - dont document it, file the bug and fix it". The bug is fixed in
damocles's separate PR, which also rewrites this page's
`list_schedules()` section.
So both of my paragraphs about scoping go: the "not scoped at all"
sentence in the intro (documenting the bug, which is what she
objected to) and the follow-up in the `list_schedules()` section. That
section is now byte-identical to main again, leaving it entirely to the
PR that changes the behaviour — the two PRs no longer touch a common
hunk in this file.
What stays is the claim this PR was actually filed for: the page said
"All scheduling ops go through the operator approval queue", and only
creating one does. The intro now splits creating from the other four
verbs and states the one authorization rule that covers all of them,
which the scoping fix makes true.
`docs/tools/scheduling.md` said three things about who may do what. Two
were wrong prose; the third was the code.
"All scheduling ops go through the operator approval queue" — one of the
five does. Cancel, edit, list and fire are `require_group("scheduling")`
and nothing else (socket_server/mod.rs:594-643), which is what the MCP
tool descriptions already tell an agent. The page pushed in the cautious
direction: someone watching a runaway recurring schedule would wait for
an operator rather than cancel it themselves.
The authorization sentence covered "read/cancel/edit". Cancel, edit and
fire really do check `cancel_authorized` (self, operator, or subtree) from
three call sites. `handle_list_schedules` takes no requester at all and
returns every row — now stated, along with the part that matters: the
snapshot includes other agents' schedule bodies.
`lines` was documented as "host-capped at 500" and the 500 was in the
agent's own MCP layer, not the host; `handle_get_logs` passed any u32
straight into JournalQuery. A limit in the caller is not a limit, so the
host clamps instead of the sentence changing. That also makes args.rs's
arg doc and the tool description agents read correct, untouched. The
sibling `get_host_journal` already clamps host-side at 100, which is both
the precedent and the control that the missing clamp here was real.
Closes#4230.
sso.md's "What this doesn't do" list opened with "It doesn't disable
local login" without qualification, which was wrong for Grafana before
the previous commit and is wrong in a different way after it: Grafana now
disables the form for every deployment, not just where authelia happens to
be on the same host.
Names the exception, gives the reason a reader can act on (a default
`admin`/`admin` account on a gateway-published vhost), and states the
consequence plainly — SSO is the only door, so a dead provider locks
everyone out, which is why the OIDC role defaults to Admin.
`matrix.md` documents the exact text of every inbound matrix signal —
three wake-body shapes and the invite loose-end — and none of them
mention that the daemon prefixes `[acct:<name>] ` when it serves more
than one account.
`wake::tag_account` is live on both documented paths (`timeline.rs:75`
for unread wakes, `:139` for invite todos), so an agent with an extra
account receives `[acct:ccc] [matrix] @a:s in #x: hi` where the page
promises a body starting `[matrix]`. The example is not hypothetical:
the matrix module uses `matrix-token-ccc` on dmatrix as its worked
example of an extra account.
It stayed invisible because the `None` arm returns the body unchanged,
so every single-account agent sees the documented format exactly. The
page is right for almost every reader and wrong for precisely the
readers its "Multiple accounts" section is written for.
Two placements rather than one. The prefix itself goes next to the wake
formats it corrects, with the worked example and the reason a leading
`[matrix]` match works until a second account exists. A forward pointer
goes in "Multiple accounts", because that is the section someone
configuring extra accounts actually lands on, and it previously covered
only the outbound `account` parameter — the half you pass, not the half
you parse.
Closes#4243.
The security section said hive→swarm ingest auth was "planned" and that the
`hive` label "becomes" unforgeable. Sixty lines later, `### Authenticated
ingest` in the same file describes it as shipped and mandatory — "No
unauthenticated mode exists" — and the code agrees: swarm-otel.nix:157 derives
one receiver port per entry in `swarm.hives`, :1387 stamps `key = "hive"` from
the receiver that accepted the sample, :751 asserts `swarm.hives != {}`, and
otel.nix:659 makes a hive without an identity a build error.
Not drift. Ordered by position in main rather than by author date, the "planned"
wording is the NEWEST of the three commits: `9dc60061` documented authenticated
ingest and `9bd2b9e9` dropped the unauthenticated mode (both 2026-08-19), while
`5fcd2a93` — authored 00:59 that morning, merged on 08-30 — added the sentence
eleven days later into a tree where the feature already existed. A long-lived
branch's prose is a snapshot of the tree it was written against, and nothing
re-reads it at merge time.
Matters because §"what the agent→collector hop is and isn't" is the page a
reader goes to for "can a hive forge a label". It answered "auth is planned",
which reads as "`hive=` is forgeable today". The truth is the opposite and
stronger.
Closes#4216.
handle_list_schedules took no requester and returned every schedule
unfiltered, unlike cancel_schedule/edit_schedule/fire_schedule_now
which all gate on the shared ownership predicate (self, operator, or
subtree via topology::is_descendant_of). list_schedules now filters
through the same predicate, renamed cancel_authorized ->
schedule_authorized since it backs all four verbs now, not just
cancel.
Fixed five stale 'every schedule' / 'unfiltered' claims found while in
here: filter_ghost_schedule_targets's doc comment, the list_schedules
MCP tool description, docs/tools/scheduling.md's per-verb section
(already self-contradicting its own top-of-file subtree-scoping claim
before this fix), and hive-core-agent-sock's ListSchedules/Schedules
wire-type doc comments (including a stale '(privileged)' marker from
the pre-topology-subtree model).
Credit to atlas: independently found the same fix while finishing
PR #4233 (which documents this bug per mara's 'fix it, don't document
it' ruling) and caught two stale doc spots I'd missed
(hive-core-agent-sock's comments) plus proposed the
schedule_authorized rename. Compared diffs directly before either of
us pushed; he dropped his scheduling.rs changes so we didn't collide.
fixes#4237
`security.md` introduces its hive-priv table as "Narrow interface —
`PrivRequest` variants map 1:1 to specific known operations". The table
named 17 of 34, so the claim it was making was only half-checkable by
the reader it was written for.
Two whole subsystems were absent rather than stragglers: btrfs
subvolume + snapshot management (9 verbs) and per-agent external forge
accounts (2), plus `SendAgentSnapshotToFd`, which passes a file
descriptor across the boundary via SCM_RIGHTS — the kind of verb
someone reads a security page to find.
The table had already been resynced by hand once, in June, and drifted
again within three months. mara's call was to keep it exhaustive rather
than summarise by subsystem: the swarm-level operations are expected to
go away, so the row count is at its peak now and shrinks from here.
Rows for the 17 missing variants, each naming what the helper actually
runs. Three of them carry the constraint that makes them safe rather
than just the command, because that is what the surrounding section is
for: `ControlInfraContainer`'s allowlist is the `InfraContainer` enum
(serde rejects unknown names at the wire boundary, and `hive-c0re` has
no variant); `WriteAgentExtraForgeAccount`'s `label` reaches a filename
and is validated as a plain identifier first; `SendAgentSnapshotToFd`
requires exactly one descriptor and refuses one arriving alongside any
other operation.
Prose is active voice throughout the new rows — "hive-priv validates
`label`" rather than "`label` is validated" — since the question a
reader brings to this table is which component enforces what.
Verified with a variant-vs-page diff carrying its own controls
(extractor count, page-readable, a present variant resolves, an
invented one does not): 34 named, 0 absent. vale is unchanged from
main's baseline for this file, 0 errors and 13 warnings on both sides.
Closes#4222.
#4184 gave 'pr status' a positional PR-number arg (--pr <n> still works, just
no longer the recommended form). docs/tools/forge.md's examples and the
prose section still showed the old --pr form in four places; same one-line
hint baked into every agent's own system prompt (hive-agent/prompts/
system.md). Neither file was touched by #4184 itself -- leftover from my own
#4182 branch that lost the collision to atlas's #4184 (docs/tools/forge.md
and hive-agent/prompts/system.md were the two genuinely non-overlapping bits
of that branch, tracked as a follow-up rather than dropped).
docs/tools/forge-cli.md is clap-generated and doesn't need regenerating --
prose-only doc changes don't touch the clap tree it's derived from.
refs #4182
`docs/tools/subagent.md` and `docs/tools/bash.md` both described their MCP
server as injected "unconditionally". Both entries are `lib.mkDefault`, and
the module says why one line above each: "so an agent.nix can still
override/disable the entry", "so the operator's own agent.nix can override
the entry".
The word matters for the subagent one in particular. The same comment block
records the framing that it is default-on for now and should become a real
capability gate later, so "can I turn this off today?" is a question an
operator has — and "unconditionally" answers it as "patch nix/" when the
answer is one override in agent.nix.
Both pages now say default, and say what the default yields to.
The other direction on the same page: `subagentHttpPort`'s option
description and the unit comment beside it both listed three tools,
`start`/`continue`/`interrupt`. The daemon serves four. #4101, which
introduced it, is titled with the three-verb phrasing, so `status` landed
afterwards and never reached either description — while `subagent.md` had
the full set all along. The option description renders into the generated
options doc, so it is the one an operator reads.
Closes#4231.
`require_descendant` (`socket_server/mod.rs:666`) authorises
kill/start/restart/update/get_logs with `topology::is_descendant_of` — the
caller's whole subtree, itself included. That has been true since `53b4e752`
(#1865), whose message says "a parent owns its whole subtree; the root covers
every agent as a consequence, no positional privilege", and two tests pin it
(`is_descendant_of_in_grandchild`, `is_descendant_of_in_self_is_true`).
The prose never followed. The four lifecycle tool descriptions, their
`// IMPORTANT:` comments, `docs/tools/lifecycle.md`, the tools README,
hive-agent-mcp's README and the system prompt every agent is rendered from all
still said "direct children only" — while `list_containers`, four tools away in
the same file, said "direct children + their subtrees".
`lifecycle.md` also taught the model #1865 deleted: "Privileged agents (for
example ruth) may operate on any sub-agent — the topology scope applies to all
others." There is no privileged class to belong to; ruth reaches every agent
because the check is transitive and everything sits under it.
Same drift on the state-query side: `resolve_agent_state_target` is
subtree-scoped by the same commit, so `get_loose_ends`' argument doc, the
`QueryAgentState` capability doc and `docs/turn-loop/mcp.md` were all telling a
parent it needs a capability to read a grandchild's threads.
Two smaller corrections found on the way:
* `list_containers` returns the caller itself. `is_descendant_of` is true for
`candidate == ancestor` and `handle_list_descendants` filters the topology
with it; called from a leaf agent it answers one row, that agent.
* `request_init_config` accepts any unused name — the requester becomes its
parent — or an existing agent already in the caller's subtree, not "a direct
child". The editing surface is narrower than the guard, though: only direct
children's config repos are bind-mounted, so re-seeding deeper in the subtree
leaves no local copy to edit. `lifecycle.md` now says so.
The prompt's other stale claim, the dead `request_apply_commit`, is #4226 and
was fixed independently by damocles in #4227 while this was being gated. This
branch keeps only the scope wording on that line.
Closes#4225.
README.md described `swarm.hives.<name>.domain` two ways, 100 lines apart. §
*Hive identity config* says it defaults to `<name>.<swarm.domain>` and shows a
names-only directory; § *The swarm's hive directory* called it "required per
entry and deliberately undefaulted" and wrote a domain into every entry.
The module sides with the first (`nix/host-modules/swarm.nix:125`):
default = if swarmCfg.domain == null
then "${name}.invalid"
else "${name}.${swarmCfg.domain}";
and `:110` answers the stale paragraph's objection directly — the default is "a
derivation from two values an operator had to state explicitly (both are
required), not a guess".
Ordinary drift, two hours wide: `433b2940` wrote "deliberately undefaulted" at
20:44 on 08-05, `3b6576fa` added the default at 22:43 the same evening under the
message "a hive's domain comes out of the swarm directory". Both mine.
The example now shows what the convention actually looks like — names only, plus
one off-convention hive carrying the override, which is the case the option
exists for.
Closes#4220.
jobq.md's "Watching it happen" section ended by promising that a step which
isn't needed "shows as `·` rather than dropping out of the tree entirely, so
the same kind of operation keeps a recognizable shape run to run".
In that view it does drop out. `JobqGraph.tsx:94` sets
`DEFAULT_HIDDEN_STATES = {Done, Skipped}` under the comment 'Product call:
"default selection filters out skipped and done"', `:299` seeds the selection as
everything except those, and `:271` puts the selection in the query string — so
the hidden states are never fetched, not merely styled out. That component is
what renders both surfaces the page names, and neither passes an override.
The claim is true of the data and false of the screen, in a section about what
is on the screen. Keeping both: the node stays in the graph, and the default
filter is named, along with the part that would otherwise surprise someone
debugging it — the selection is a request parameter.
Closes#4223.
`5478e0bf` (`fix(#3554)`) made both store exporters unconditional and removed
the "somewhere to send" assertion, and touched no documentation — its diffstat
is four `nix/` files. `services.md` § *Telemetry collector (OTEL)* still
described the old shape in two places.
① "it writes the store above and exports to `otel.endpoint`, doing both when
both are configured" — only the upstream is conditional. `exporterNames`
(swarm-otel.nix:231) always carries the store exporter, with a comment saying
why: `deploy.victoriametrics.enable` means "this host RUNS the store", and a
swarm has one either way.
② "With neither `otel.endpoint` nor the store enabled, this module refuses the
collector at eval" — that assertion is gone. swarm-otel.nix:693 records the
removal at the head of the assertions list, and `5478e0bf`'s message states it
was a ruling rather than an oversight: a collector on a host of its own is a
supported shape, so refusing to build it would have made the fix illegal
exactly where the bug bit hardest. `git grep` finds no surviving assertion of
that shape.
The replacement paragraph states the invariant an operator can act on — the
exporters have no gate, the stores are addressed by swarm name — and keeps the
failure it prevents, since "an absent exporter is not an error" is the part that
made the old bug silent.
The stale claim dates to `80c9118f` (2026-08-18), 13 days before the behaviour
changed under it.
Closes#4214.
secrets.md said the swarm collector's OIDC secret is copied "when authelia is
enabled on this host and something published is being scraped; otherwise no
secret is needed and none is placed". The second condition left the tree in
5478e0bf (#3554): `swarm-otel-oidc-secret` and the client registration it
delivers for are both `lib.mkIf deployCfg.authelia.enable`, and
`publishedScrapeTargets` appears in neither guard. swarm-otel.nix:589 says why
in as many words — authelia refuses a bearer-authz client with no audience,
which is what the old guard was for, and the push audiences are unconditional,
so there is now always one.
The sentence survived because I rewrote the paragraph around it a day later
(8cba57e0) for the collector-elsewhere case and carried the clause through.
What a reader loses: with authelia on and nothing published, the page promises
the delivery is inert. It is not — the unit runs, waits a bounded 120s for
authelia's mint, and fails the collector's container start if the secret never
appears. So the case documented as quiet is one that can fail a boot, and the
operator debugging it has a page saying this path was not taken. The
replacement states the single real condition and that failure mode.
Closes#4210.
security.md's "What's NOT exposed" list said `/home/<name>/.claude/` is mode
`0700`. Measured from inside an agent container — the vantage the claim is
about, since the nixbld users in this threat model run there — it is `0755`.
`ensure_claude_dir` (hive-c0re/src/lifecycle/setup.rs) chmods it to 0755
deliberately, and its comment says why: hive-core is a different user and needs
read+execute to list the directory so `claude_has_session` can detect a valid
session. So the code is right and the doc named a mechanism that was never the
real one.
The conclusion the doc drew still holds — nixbld users can't read anything that
matters. Every sensitive entry is protected on its own: `.credentials.json`,
`history.jsonl` and `settings.json` at 0600, `projects/` and `sessions/` at
0700, and the five `backups/*.backup.*` at 0600. 32 nixbld users exist on this
host (uid 30001+) and none of them can read any of it.
What changes is the invariant a reader should rely on. The directory mode was
the doc's whole stated guarantee and it isn't there; the protection is per-file,
which fails differently — anything landing in `.claude/` at a default mode is
world-readable while the doc says it cannot be. `plugins/` (596 files, 0755) and
`.last-cleanup` (0644) already demonstrate that files do land loose there. Both
are harmless; the point is that nothing stops the next one from being.
Closes#4204.
`cfg` is whatever the reading module bound it to. It does not exist in a
NixOS configuration, so a sentence naming an option as `cfg.<name>` is
correct about behaviour and unusable as an instruction — the reader has
to go find the real path.
The four sites in docs/networking/gateway.md this was filed for:
cfg.sshPort -> services.hyperhive.swarm.forge.sshPort
cfg.dashboardPort -> services.hyperhive.c0re.dashboardPort
cfg.frontend (x2) -> services.hyperhive.c0re.frontend
Sweeping docs/ for the pattern rather than the ticket's line numbers
found five more, in four other files:
approvals.md cfg.hyperhiveFlake -> services.hyperhive.c0re.hyperhiveFlake
matrix.md cfg.registrationTokenFile -> services.hyperhive.deploy.matrix.registrationTokenFile
matrix.md cfg.gatewayHost -> services.hyperhive.swarm.matrix.gatewayHost
conventions.md cfg.dashboardPort -> services.hyperhive.c0re.dashboardPort
gotchas.md cfg.dashboardPort -> services.hyperhive.c0re.dashboardPort
matrix.md is the clearest case for doing this at all: its two `cfg.`
references resolve to *different* option trees — `deploy.matrix` and
`swarm.matrix` — so the shorthand is ambiguous even within one file.
Each path is read off the `mkOption` that declares it plus the
`options.services.hyperhive.*` root it sits under, with the indentation
checked so a nested block cannot have been missed. `cfg.frontend` is
declared in hive-c0re, not the gateway: the gateway module binds
`cfg = config.services.hyperhive.gateway`, which has no `frontend`.
Deliberately unchanged: docs/networking/snapshot-store.md:136, where
`cfg.port` sits inside a ```nix block quoting module source. `cfg` is
correct there, and rewriting it would make the snippet wrong.
Closes#4193.
The TLS section read as universal — "the gateway always terminates TLS",
"listens on httpsPort on every vhost". Measured against the rendered
config of an all-local host: ten of eleven vhosts carry two listens and
addSSL/forceSSL, and `grafana-metrics` carries one plain-HTTP listen on
127.0.0.1 with a single `= /metrics` location proxying grafana's unix
socket for the local collector.
The exception is right; a doc with no room for it is not. Someone asking
"is anything here serving plain HTTP" was told no, when the useful answer
is "no, except an internal class that is loopback-bound and deliberately
exempt" — which tells them what to look for on the next host.
One sentence where the modes are introduced rather than a patch to each
sentence that inherits the assumption, plus "every vhost" -> "every
routable vhost" where the self-signed default repeats it.
Two neighbouring claims were checked at the same time and hold: /_matrix/
proxies to 127.0.0.1:8008, and .well-known/matrix/client returns the
homeserver base_url with no port suffix on 443.
Closes#4191
`docs/tools/forge-cli.md` is generated from the clap tree, so changing an
argument or its doc comment changes that file too. The freshness check in
`checks.hive-forge-docs` is what says so, and it is what failed here —
after 8m44s, on a PR that was already approved.
Regenerated, no hand edits. Two hunks, both the `pr status` section, and
`nix fmt` leaves the result byte-identical so the formatter and the
generator are not fighting over it.
Part of #4182.
observability.md scoped the whole feature to Claude Code statistics, so a
reader asking what leaves an agent container would have concluded
counters and nothing else.
That page also documents the trust properties of the agent->collector
hop, and those are what needs restating for a richer payload: same
destination, same credential, same already-accepted ability to push
arbitrary OTLP. What changes is how much detail leaves by default.
docs/swarm/services.md needs no change. It already said each hive ships
the journals of "the harness units inside every agent container" — which
was not true until now.
Part of #3940.
The `prose lint (vale, errors)` CI gate fails on Microsoft.Contractions
at `setup.md:147`, in prose this branch adds.
Verified against the repo's own styles rather than assumed: with
`XDG_DATA_HOME=$PWD/.vale-data vale sync` (the ini's own note — sync
ignores `StylesPath` and writes under `$XDG_DATA_HOME`), the file
reports 0 errors, and a copy of it with the word put back reports that
line and nothing else. Without the sync, vale exits 2 on a missing
styles dir, which is a config error rather than a pass.
The bootstrap unit writes a policy granting `secret/data/swarm/agents/*` and
nothing creates that engine. A fresh OpenBao has no `secret/` — only a dev-mode
one does — so `swarm-controller`'s first credential write answers `no handler
for route "secret/data/swarm/agents/<agent>/matrix/<name>". route entry not
found.` Measured on the live host at 21:27:27Z; #4171.
`git grep` for `secrets enable`, `kv-v2`, `kv_v2` and `sys/mounts` returned zero
across the whole tree. Control, so the zero means something: `auth enable` in
this same file returns 2 — the same defect was already found and fixed once, for
the cert auth mount, with a comment that states the principle. This is the other
half of it.
The mount name is now bound once and interpolated into both the policy text and
the new step, because a grant and a mount that disagree is exactly the failure
being fixed.
Placed outside the client-CA block: the controller writes *through* this mount
regardless of whether anything can log in by certificate. `module-eval` asserts
that, since one indentation level decides it.
Grants, measured against a real openbao 2.6.2 rather than derived:
`-output-policy` asks for `sys/mounts/secret` create+update, and a token holding
exactly `sys/mounts` read + `sys/mounts/<path>` create/update enabled the engine
— **no `sudo`**, unlike `sys/auth/cert`. Negative control: the same token on an
ungranted path got 403, so the grant is what made it work. `setup.md`'s
documented policy gains those two.
Also from that session, each deciding how this is written: re-enabling an
existing path errors (exit 2), so this asks first like the auth mount does;
`secrets list -format=json` keys look like `"secret/"`, so the `case` idiom
ports over; and `kv put -mount=<p>` reports `<p>/data/...`, confirming v2 — the
prefix the policy grants and the client writes.
setup.md also drops a check that cannot work: it told the operator to confirm
with `bao read auth/cert/…`, which 403s because the host wrapper carries no
token. `systemctl status swarm-bao-controller-policy` needs no credential and
names the three success lines. The first-attempt-after-rebuild race is now
written down too — the store is still coming up, and the 30s retry is what
lands.
Refs #4171.
The bootstrap block still described a one-shot unit "inside the store's
container". That stopped being true when the unit moved, and a merged
change that leaves the docs asserting the old topology is worse than one
that never moved it.
Also retires the "only run once, didn't go all the way through" note. It
has run twice against a live store now and failed both times — but for
its own reasons, not the grants': it could not reach the store from where
it ran, so neither attempt exercised a single one of them. The grants are
still derived from `-output-policy` rather than proven, and the warning
now says that directly instead of implying a partial test.
The host now has a wrapped bao (#4154, deployed) carrying the store's
address, CA and client cert, so the init step is a bare command there
instead of a root-login hop with an SNI override. The container route
stays as one sentence explaining why it needs the two extra pieces.
The delete step said "once that has run" without saying what -- the
rebuild, or the unit succeeding. It now names the check that settles it,
because deleting the token before the role exists leaves the unit
skipping forever and looks exactly like a store nobody bootstrapped.
Closes#4162
The doc-comment sweep changed --help text, so the generated reference went
stale and hive-forge-docs-fresh caught it. Regenerated with the command the
check itself prints; the diff is five lines, all propagations of the sweep.
Kept as its own commit rather than an amend: argus approved d090a9fb, and a
force-push clears both the verdict and the review request.
Refs #4155
docs/tools/forge.md already listed the nine renamed verbs as removed, then
used them ~30 more times in pasteable blocks. Sweeps every occurrence a
reader would type, including three runtime messages that told the user to
run a verb the same binary rejects.
The renamed-verb list itself keeps the old names; it is what documents them.
Refs #4155
mcp.md called it `hivectl choom`; the verb is `hivectl agent <name>
choom`. observability.md cited `hivectl shell`, which has never existed
at any level — the sentence only needs a shell, so it names one instead of
guessing which command an operator reaches for.
Found by subtracting the generated hivectl-cli.md's command headings from
every hivectl citation in docs/. Re-running it after the fix drops the
candidate list from 16 to 14, losing exactly these two.
Closes#4159
"On the host that RUNS the store" is ambiguous in the deployment we
actually ship: openbao runs inside the `swarm-bao` container, so the
host and the container are different vantages with different working
addresses, and the instruction reads as either.
From inside the container neither obvious address works — the server
certificate is issued for the store's DNS name and carries no IP SAN,
while that name resolves to the bridge rather than to the container. The
fix is an SNI override: verify the name, connect on loopback. The host's
own address is stated beside it so the reader can pick a vantage instead
of discovering the constraint from a TLS error.
Per mara's screenshot report (agent-terminal icons not aligned in the
first column) — and her explicit follow-up steer on the first version of
this fix: 'dont do it by offsets at all, it should be part of the layout
that they align.'
Root cause traced first, not guessed: .row-glyph relied on inheriting
text-indent: -1.4em from .live .row to paint its glyph in the reserved
prefix slot; details.row > summary separately reset text-indent: 0 for
its own hanging-caret needs, which also zeroed the inherited value for
the icon nested inside it (indent inherits by computed value, not by
rule). Two independently-computed offsets that had to agree by hand,
and silently didn't.
Replaced the whole offset scheme with a real structural fix: every row
is a 2-column CSS grid (icon column, width from one shared
--row-icon-col custom property, then content column), and a details
row grids its own <summary> with the exact same grid-template-columns
value instead of griding itself (so its <pre> body still stacks full
width below, not squeezed into column 2). Icon and content are placed
by explicit grid-column, not auto-placement inference, so an icon-less
row's lone child still lands in the content column. .row-content is a
new wrapper class (Row.tsx, terminal.js's row()/mutableRow()/
placeholder()) giving that content an element the grid can target by
class - a DocumentFragment (what linkify() returns) doesn't persist as
a node once appended, so without an explicit wrapper there was nothing
for the grid to place.
Caught and fixed a second real bug while building this for real (not
just reasoning about the CSS): the first draft's details.row { display:
block } had lower specificity than .live .row's display: grid and never
actually applied, squeezing <summary> into the row's own 1.4em icon
column and wrapping its text one character per line. Needed
.live details.row to out-specify it.
Verified with a headless-chromium render of all 6 row shapes (flat
icon / flat icon-less / flat markdown-body / details icon / details
icon-less / a long-wrapping flat row) - all align and wrap correctly.
Also verified via the earlier Range.getBoundingClientRect() measurement
(glyph paint position, not just the element's own box): diff 0.00 for
both this and the previous fix, but only this one is structural rather
than two numbers that happen to still agree today.
mara, on #4128: add a CI step (separate from the existing one) that
only flags error-level vale hits, so it can be made a required check
without also gating merges on the pre-existing warning/suggestion
backlog. The full job stays as-is, advisory, for that backlog.
Also brought docs/scheduler/ci.md's CI-checks table back in sync with
reality (it was missing the doc-pointer lint job already; now lists
both vale jobs and their required-check status).
mara disagreed with exempting this section from #4041's ruling.
'Backend' here meant 'which LLM API provider', and the doc already
uses 'provider' for the same concept a few lines down ('provider-
specific model string') - so reword to that instead of carving out
an exception. Drops the inline vale suppression comment entirely.
Per #4128 (mara: allow-everywhere false positives go in a central
list, otherwise fix in source). Testing surfaced better fixes than
the plan posted on the issue:
- 5x Microsoft.Contractions 'that is' idiom false positives: adding
the missing comma ("that is, ...") both reads better and satisfies
the rule's own negative-lookahead, so no suppression is needed at
all. Fixed in docs/integrations/forge.md, docs/tools/forge.md,
docs/tools/hivectl.md, docs/web-ui/dashboard.md, and
swarmctl-cli.md's generated source (swarmctl/src/main.rs, doc
comment regenerated via markdown-docs).
- persistence.md's 'is not' matching inside 'is nothing': reworded to
'there'\''s nothing' rather than add any exception -- dodges the trap
and is a genuine contraction besides.
- ca.md's 'it is' matching inside the already-correct 'it isn'\''t':
tried a central .vale.ini TokenIgnores entry first per the
allow-everywhere framing, but testing against the real file (not
just a synthetic snippet) found it silently fails to suppress
whenever markdown emphasis syntax appears earlier in the same
file -- an offset-drift bug in how Vale applies TokenIgnores, not
a config mistake. Reworded to "it'\''s not" instead, same fix
shape as persistence.md.
- config.md's 3 genuine Microsoft.Avoid 'backend' exceptions (already
flagged and accepted on #4139 -- an actually-pluggable LLM API
provider, matching the nix option's own name, not one internal
system to name): scoped inline vale suppression around just that
section, since this one really is context-specific rather than a
rule bug.
Verified: fresh 'vale docs/ --minAlertLevel=error' is 0 errors AND
0 warnings (was 10 errors). nix fmt 0 changed beyond the edits
themselves. pre-push lints (tracker-tag/comment-block/doc-pointer)
clean. cargo clippy -p swarmctl -- -D warnings clean. Diffed the
regenerated swarmctl-cli.md against the old copy to confirm only
the intended line moved.