docs: fix genuine passive-voice hits in docs/swarm

Read all 94 write-good.Passive hits across docs/swarm/ (ca.md,
README.md, secrets.md, services.md, sso.md, ui.md) in context. 44 are
genuine catches with a nameable, usually already-established actor
(swarm-controller, authelia, swarmctl, the controller, the gateway,
this module, hyperhive itself, or 'the operator' for manual actions) —
rewritten to active. 50 are legitimate passives or false catches, left
alone: predicate-adjective state descriptions (is expected/misconfigured/
broken), negative-capability idioms (no X is needed/placed, can't be
Yed/listed/fetched), config-state conditionals (whenever/when X is
enabled/configured/set), requirement-list labels (is required),
'is tracked as' idiom, backward-looking changelog facts with no actor
(was removed/verified/introduced), ambiguous-actor statements left
conservatively alone (agents are created and destroyed — could be
hive-c0re or swarm-controller, doc doesn't say), and a couple of
deliberately-parallel idiom pairs.

Several sibling-inconsistency fixes: a passive clause sitting next to
an already-active sibling describing the same fact/mechanism (ca.md's
two-bullet consumer list, README's 4-item WireGuard-mesh bullet list,
README's controller-registers-hooks paragraph, sso.md's followed-a-302
sentence).

Verified via vale on the whole directory, diffed against main's exact
baseline (not just the Passive count): write-good.Passive 94 -> 50
exactly, every other category unchanged (1 pre-existing
Microsoft.Contractions error at services... at secrets.md:182,
8 TooWordy, 1 Microsoft.We, 1 Microsoft.FirstPerson — same counts,
same locations).
This commit is contained in:
iris 2026-09-08 13:37:57 +02:00 committed by mara
commit fab2a0dedc
6 changed files with 79 additions and 78 deletions

View file

@ -41,7 +41,7 @@ services.hyperhive = {
```
`swarm.domain` and `hiveName` are **required** whenever hyperhive is
enabled; eval fails with a hint naming each. Neither is defaulted,
enabled; eval fails with a hint naming each. Neither defaults,
because a guessed value here is a wrong hostname that evaluates cleanly
and deploys — an eval failure asking the operator to write the address
down is the cheaper outcome. **Upgrading past this release means setting
@ -55,9 +55,9 @@ the other hives read — `swarm.hives.edge.domain = "edge.elsewhere.example";`.
Setting `services.hyperhive.domain` directly still works and still wins,
with a **deprecation warning**. The reason it's deprecated isn't tidiness:
that option is local to one host, the directory is copied to every host,
so a value written only there leaves every peer pointing somewhere else
with nothing detecting the disagreement.
that option is local to one host, and the operator copies the directory
to every host, so a value written only there leaves every peer pointing
somewhere else with nothing detecting the disagreement.
⚠️ **Upgrading:** a hive that has been running on `swarm.domain` +
`hiveName` alone now needs its own directory entry —
@ -81,8 +81,8 @@ and `qualify()` / `qualified_label()` semantics.
## Swarm CA
A hive's internal TLS chains to a **swarm root CA**, so a peer that
trusts the root validates every hive in the swarm rather than being
pinned to each one by hand. Provisioning modes, what to hand a peer
trusts the root validates every hive in the swarm rather than pinning
to each one by hand. Provisioning modes, what to hand a peer
(`trust-bundle.pem`, never `ca.pem`), the name constraints on a hive
CA, and how an existing hive adopts the hierarchy: [`ca.md`](ca.md).
@ -107,8 +107,8 @@ which of the three topologies makes it the operator's job to place:
## Swarm UI
The operator-only web surface on the swarm apex, why reaching it needs
the `admins` group rather than just a session, and the four sites a
swarm service name has to be wired into: [`ui.md`](ui.md).
the `admins` group rather than just a session, and the four sites you
wire a swarm service name into: [`ui.md`](ui.md).
## The swarm's hive directory
@ -249,15 +249,15 @@ services.hyperhive = {
### What the mesh does
- `networking.wireguard.interfaces.wg-hive` is configured on the host
(not inside agent containers; containers reach peers via the host's
routing table).
- UDP port 51820 (or `listenPort`) is opened on the host firewall.
- hyperhive configures `networking.wireguard.interfaces.wg-hive` on the
host (not inside agent containers; containers reach peers via the
host's routing table).
- It opens UDP port 51820 (or `listenPort`) on the host firewall.
- `swarm-wireguard.nix` reads each entry's `wireguardAddress` directly
from `services.hyperhive.swarm.peerHives` to build `wg-hive`'s
`allowedIPs`, so intra-swarm traffic can route over the mesh address
rather than the public domain.
- `persistentKeepalive = 25` is set by default; override or null to
- It sets `persistentKeepalive = 25` by default; override or null to
disable (not needed when both sides have public IPs and no NAT).
### NAT / one-sided endpoints
@ -310,7 +310,7 @@ below. A hive without them reads
| `fresh` | nothing — reported within `staleAfterSeconds` |
| `stale` | the hive stopped reporting. Its last payload is still shown, so check `age_seconds` and the payload for what it managed to say |
| `never_reported` | this hive has never reported at all — normally a deployment that hasn't happened, not an outage |
| `unknown` | something is publishing under a name that's not in `swarm.hives` — a typo in the roster, or a hive that was removed and is still running |
| `unknown` | something is publishing under a name that's not in `swarm.hives` — a typo in the roster, or a hive removed from the roster but still running |
Every row also carries `last_seen_unix` and `age_seconds` if you want to
apply your own threshold. The timestamp is the one the queue recorded on
@ -382,10 +382,11 @@ controller's — and **both are expected; don't delete either.** Removing
a hive's stops it acting on config PRs; removing the controller's just
gets recreated on its next start.
Nothing to configure. The hooks are registered only when this host also
serves the swarm UI vhost — that's what publishes the endpoint, and a
hook the forge can't reach would collect failed deliveries while
looking healthy. The HMAC secret is generated on first start and kept
Nothing to configure. The controller registers the hooks only when this
host also serves the swarm UI vhost — that's what publishes the
endpoint, and a hook the forge can't reach would collect failed
deliveries while looking healthy. The controller generates the HMAC
secret on first start and keeps it
(see [`docs/agent-lifecycle/persistence.md`](../agent-lifecycle/persistence.md)).
To check it's working, push to `internal/knowledge` and look for

View file

@ -3,7 +3,7 @@
A hive's internal TLS chains to a **swarm root CA**: the root signs each
hive's own CA, and that hive CA signs the gateway leaf. A peer that
trusts the root once validates every hive in the swarm, present and
future, instead of being pinned to each one by hand.
future, instead of pinning to each one by hand.
That's the whole point of the hierarchy — it turns per-peer trust from
O(n²) hand-pinning into one anchor per swarm.
@ -55,8 +55,8 @@ able to issue, but not an operator-installed sub-CA in
by any other means. If you have arranged one, the warning is expected
and can be ignored. Otherwise either install the sub-CA, or pin the
names back under the hive domain — a supported migration, since the
sub-CA is constrained to the _configured_ names and the swarm root
carries no name constraints at all.
sub-CA carries a name constraint on the _configured_ names and the
swarm root carries no name constraints at all.
## Constraints on the material
@ -89,9 +89,9 @@ host:
scp /var/lib/swarm-ca/root.pem <peer-host>:/var/lib/swarm-ca/root.pem
```
That's the point of the hierarchy: the root is installed **once per
swarm host**, not once per peer, so a hive joining later needs no edit on
the hives already running. A hive whose cert already chains to a public
That's the point of the hierarchy: the operator installs the root
**once per swarm host**, not once per peer, so a hive joining later
needs no edit on the hives already running. A hive whose cert already chains to a public
CA has nothing to install — `peer-config` says so instead.
Handing a peer this hive's own `ca.pem` would not work even as a
@ -114,8 +114,8 @@ two cases.**
Adoption happens by itself, once. `hive-tls-ca.service` notices that
`ca.pem` doesn't chain to the root, keeps the old certificate as
`ca-previous.pem`, and re-issues under the root; the next leaf is signed
by the new CA.
`ca-previous.pem`, and re-issues under the root; the new CA signs the
next leaf.
It's safe to automate here precisely because this is the
all-on-one-host shape: every consumer is on this box, so "when will they
@ -142,7 +142,7 @@ systemctl restart hive-tls-ca.service
```
Failing rather than warning is deliberate: a hive whose CA doesn't
chain to the root it has been given is misconfigured, and a warning in a
chain to the root it received is misconfigured, and a warning in a
build log isn't something anyone reads twice.
To keep the current CA on purpose — a hive that deliberately stays
@ -162,12 +162,12 @@ beside the key under `swarm.ca.stateDir` — and that has a cost worth
naming, because it's subtle and it bites at a distance:
> **Nothing whose trust store is assembled at build time can reference
> the swarm root.** `security.pki.certificateFiles` is read inside the
> derivation; the root doesn't exist there.
> the swarm root.** The derivation reads `security.pki.certificateFiles`
> inside itself; the root doesn't exist there.
Two consumers, and only one of them is fine:
- **Agents are covered.** `hive-tls.nix` folds the root into this hive's
- **`hive-tls.nix` covers agents.** It folds the root into this hive's
`trust-bundle.pem`, hive-c0re receives that path as
`HIVE_TLS_CA_PATH`, and the meta-flake renderer embeds that one file
next to every agent's flake. The bundle is the runtime-to-build-time

View file

@ -8,7 +8,7 @@ Two rules run through all of it.
**Private key material and access tokens are paths, never values.** Every option
carrying one takes a file path (`*File`), because a literal written into a nix
expression is rendered into the nix store — world-readable and permanent. No
expression ends up in the nix store — world-readable and permanent. No
option in this tree accepts one inline, and adding one would be a leak rather
than a convenience.
@ -27,7 +27,7 @@ container that starts after it.
## Topologies, by who places secrets
Every row below is read against one of these. This is a different cut
Read every row below against one of these. This is a different cut
from the [deployment shapes](services.md#deployment-shapes) --- those
say _where services run_, these say _who is responsible for a secret
file being there_ --- the two lists don't line up one-to-one, and
@ -58,7 +58,7 @@ neither is a renaming of the other.
| the secret store's own contents | openbao, on first `bao operator init`**an operator action, not a unit** | inside the `swarm-bao` container, at its own `/var/lib/openbao`, kept across rebuilds by `ephemeral = false`. ⚠️ **Not a host path**: `nixos-container destroy swarm-bao` takes the raft data with it, so back up the container's tree, not `/var/lib/`. Only the store's TLS material (`/var/lib/swarm-bao-tls`) and its PKCS11 token (`/var/lib/swarm-bao-token`) are host-level | n/a — there is one store; a hive elsewhere is a _client_ of it and holds none of this |
| the secret store's unseal material | the HSM/TPM under `deploy.bao.seal = "pkcs11"`; openbao itself under `"shamir"` | in the token; or held by whoever ran `bao operator init`, which is what `"shamir"` means and why it's stated rather than inferred | n/a — only the host running the store seals anything |
The three keys authelia mints for itself are generated in-container precisely
Authelia mints the three keys for itself, in-container, precisely
because nothing outside that container ever reads them. **That's the test worth
applying to any secret added here** — and the client secret's plaintext half is
the one row that fails it, which is the entire reason a delivery step exists.
@ -71,8 +71,8 @@ an all-local swarm it reads authelia's file where it lies and no second copy is
made. On any other topology it's an ordinary "operator provides the file"
case — see `services.hyperhive.otel.clientSecretFile`.
The **swarm's** collector does need one. It runs in a container, so its copy is
placed by `swarm-otel-oidc-secret.service` and lands at
The **swarm's** collector does need one. It runs in a container, so
`swarm-otel-oidc-secret.service` places its copy, landing at
`/var/lib/swarm-otel-oidc/<client-id>.secret` — the same shape as the forge and
homeserver rows below, and for the same reason: the container that mints the
secret isn't the container that reads it.
@ -120,7 +120,7 @@ refuses every client rather than degrading — and a refusal reaches the client
a timeout, so the symptom is every consumer hanging with nothing logged.
One consequence of the generated path worth knowing before you debug it: with
`autoGenerateCallout` set, the queue's config is assembled at boot rather than at
`autoGenerateCallout` set, the queue assembles its config at boot rather than at
build time, so a malformed one surfaces when the container starts instead of
when the system builds. The server names the offending file and refuses to run.

View file

@ -16,8 +16,8 @@ sections below don't repeat it, so a service that stops deriving is a
visible difference rather than one more paragraph saying the same thing.
`singleHostSwarm` is the all-on-one-box switch above it: it defaults
both `deploy.allSwarmServices` and `swarm.ca.autoConfigure` (the swarm CA
is generated here). Each derived toggle can still be set on its own,
both `deploy.allSwarmServices` and `swarm.ca.autoConfigure` (this host
generates the swarm CA here). Each derived toggle can still be set on its own,
which wins, so "all local except X" needs no further option.
**Both default to off**, and that's deliberate: a host can't tell
@ -45,8 +45,8 @@ the shapes worth naming are the ones they produce:
- **Fully spread out.** One container / VM / machine per service,
somewhere.
**These are a set, not a ladder with a correct top, and in-between
shapes are supported.** Each derived toggle can be set on its own (see
**These are a set, not a ladder with a correct top, and hyperhive
supports in-between shapes.** Each derived toggle can be set on its own (see
above), which is what makes "all local except X" a configuration rather
than an unsupported edge case. Nothing in hyperhive prescribes a
deployment model, so a doc that treats one shape as the real one and
@ -69,21 +69,21 @@ provider, differentiated by roles and claims rather than by mechanism —
there is one IdP and one auth path.
- **`deploy.authelia`** — run the container here.
- **`swarm.authelia.url`** — where clients are sent to authenticate.
- **`swarm.authelia.url`** — where clients go to authenticate.
Present on **every** hive, defaulting to this host's own instance only
when this module is the thing running it; otherwise `null`, and a hive
joining someone else's swarm sets it explicitly. Null means "no SSO
configured", and consumers say so rather than guessing an address.
The users database is written by swarm-controller, not by hand: agents
swarm-controller writes the users database, not by hand: agents
are created and destroyed continuously, so the subject set is dynamic.
This module only guarantees the file exists and parses, so authelia
starts with nobody in it rather than failing to start — a provider with
no subjects yet is the correct state before anything has provisioned
them. Session and storage keys are generated in the container on first
boot and never rotated automatically; replacing one invalidates data
already written (sessions, the encrypted store), so that's an operator
action.
them. Authelia generates session and storage keys in the container on
first boot and never rotates them automatically; replacing one
invalidates data already written (sessions, the encrypted store), so
that's an operator action.
Storage is local sqlite and the notifier writes to a file. Both are
small-deployment choices, and the scope is the justification: redis
@ -91,13 +91,13 @@ buys shared session state across replicas and there is one instance;
SMTP exists to mail humans, and provisioning here is programmatic.
See [`sso.md`](sso.md) for bootstrapping the first user and the OIDC
relying-party flow, and [`secrets.md`](secrets.md) for where each of
authelia's keys is generated and read.
relying-party flow, and [`secrets.md`](secrets.md) for where authelia
generates and reads each of its keys.
### Metrics (VictoriaMetrics + Grafana)
The swarm's telemetry lands in one VictoriaMetrics and is read through
one Grafana, in two containers at `metrics.<swarm-domain>` and
The swarm's telemetry lands in one VictoriaMetrics, and one Grafana
reads it, in two containers at `metrics.<swarm-domain>` and
`grafana.<swarm-domain>`. Two containers rather than one so Grafana can
be restarted or broken without taking the time-series database with it.
@ -119,8 +119,8 @@ See `retentionPeriod` below before leaving it at its default.
| `deploy.grafana.datasourceUrl` | Only if you front VictoriaMetrics with something else. It defaults to the store on this host, which is the only thing it can reach. |
**Logging in.** Grafana is behind swarm SSO, so the accounts are the
authelia ones — there is no separate Grafana password, and the local
login form is switched off whenever SSO is configured. If you enable
authelia ones — there is no separate Grafana password, and this module
switches off the local login form whenever SSO is configured. If you enable
Grafana on a host with no authelia, the form stays on and Grafana's
default `admin`/`admin` applies; change it before exposing that host.
@ -178,9 +178,9 @@ gateway either way.
| `swarm.otel.domain` | Only to rename it — the default already resolves correctly for every hive in the swarm. |
| `swarm.otel.port` | Only if something else on the services host already claims `4319`. |
With neither `otel.endpoint` nor the store enabled, this collector is
refused at eval — a tier that receives samples and drops them looks
healthy while losing data.
With neither `otel.endpoint` nor the store enabled, this module refuses
the collector at eval — a tier that receives samples and drops them
looks healthy while losing data.
Agent-side configuration, and what a hive's own collector does, are in
[`../scheduler/observability.md`](../scheduler/observability.md).

View file

@ -5,7 +5,7 @@ provider** every protected vhost checks (`auth_request`), and — once any
client is declared — an **OIDC provider** issuing tokens to relying
parties: the forge and the matrix homeserver.
The second role is derived rather than switched:
The second role derives rather than switches:
`services.hyperhive.swarm.authelia.oidc.clients` being non-empty turns it
on. authelia refuses to start with a provider that has no clients, so a
separate `enable` would be a second fact free to disagree with the first.
@ -18,10 +18,10 @@ name all follow `deploy.authelia`, so there is nothing to turn on
separately. (Details, including why a client hive must not declare that
vhost: [`../networking/gateway.md`](../networking/gateway.md).)
**Authelia doesn't start until at least one user exists.** The user
store is generated empty — deliberately, since seeding a default account
would put a credential in a config file — but authelia validates it at
startup and treats "no users" as fatal:
**Authelia doesn't start until at least one user exists.** This module
generates the user store empty — deliberately, since seeding a default
account would put a credential in a config file — but authelia validates
it at startup and treats "no users" as fatal:
```
error reading the authentication database: could not validate the schema:
@ -47,8 +47,8 @@ password: <generated>
this password is stored nowhere — record it now
```
The password is generated, hashed, and printed once; only the hash is
kept. `swarmctl` reads and writes authelia's `users.yml` directly — it's
`swarmctl` generates, hashes, and prints the password once, keeping only
the hash. It reads and writes authelia's `users.yml` directly — it's
the one user store, shared with `swarm-authelia-bridge`, which creates
agent identities in the same file. No restart: authelia watches it. Full
reference: [`../tools/swarmctl-cli.md`](../tools/swarmctl-cli.md).
@ -84,7 +84,7 @@ Two behaviours worth knowing before you rely on them:
already was. Revocation is the exception on purpose: a typo'd group
name that reported success would leave an account holding access you
believe you took away, and that's the one outcome nobody re-checks.
- **The resulting group list is printed** because group names have no
- **`swarmctl` prints the resulting group list** because group names have no
registry anywhere. A misspelled `--add-group` creates a real group that
no access-control rule mentions, so the user gains nothing and no error
is possible — reading the line back is the only check there is.
@ -115,8 +115,8 @@ authelia's first boot to mint that client's secret and copies it into the
service's container, and the service's own module contributes its client
entry — callback URL included — to authelia's client list.
The callback is built once and read twice, so the redirect URI authelia is
told to allow and the one the service actually sends can't drift apart. A
The callback is built once and read twice, so the redirect URI authelia
accepts and the one the service actually sends can't drift apart. A
mismatch there is a rejected login with no error text worth reading.
⚠️ The delivery is a copy, not a `bindMounts` entry, and deliberately so:
@ -147,7 +147,7 @@ services.hyperhive.swarm = {
};
```
**Both are asserted at eval.** A hive that boots with SSO
**This module asserts both at eval.** A hive that boots with SSO
half-configured shows a login button that always fails — a symptom
many layers from its cause, and far worse to diagnose than an
evaluation error.
@ -173,9 +173,9 @@ Two consequences worth stating plainly:
host-side ownership arrangement to maintain.
- **Matrix SSO lives inside the homeserver.** The client-server API is
spoken by non-browser clients holding matrix access tokens — every
agent's own daemon — as well as by federation, so `/_matrix/` is served
directly and authenticates itself. The forward-auth vhosts protect
browser surfaces; this isn't one of them.
agent's own daemon — as well as by federation, so the homeserver
serves `/_matrix/` directly and authenticates it itself. The
forward-auth vhosts protect browser surfaces; this isn't one of them.
## Machine clients
@ -197,7 +197,7 @@ thing to keep in step, and it drifts silently — a mismatch presents as a
valid token refused at the target, which reads like a broken credential
rather than a broken name.
**Tokens are signed (`RS256`), not opaque**, because a resource server
**Authelia signs tokens (`RS256`), not opaque ones**, because a resource server
that can't call the provider back is a real case here: the telemetry
collector verifies offline against `/jwks.json`, and an opaque token gives
it nothing to verify. The queue's responder introspects instead — a
@ -218,15 +218,15 @@ HTML instead of the failure that actually happened:
- `/api/oidc/token`, `/.well-known/openid-configuration` — a client
parsing an error page as its JSON document
Authelia's `/api/` and `/.well-known/` prefixes are routed **without**
error interception. The split is by _audience_, not by an enumerated path
The gateway routes authelia's `/api/` and `/.well-known/` prefixes
**without** error interception. The split is by _audience_, not by an enumerated path
list: a human gets the page, every machine caller gets the status.
Enumerating endpoints individually would leave the next one added
silently intercepted.
The same shape bites any machine route behind a browser-shaped gate: a
`302` to a login page is followed, the login page answers `200`, and the
caller reports success while nothing happened. Log ingest hit exactly this
The same shape bites any machine route behind a browser-shaped gate: the
caller follows a `302` to a login page, the login page answers `200`,
and the caller reports success while nothing happened. Log ingest hit exactly this
and lost eleven hours of delivery in silence.
**Checking it, if you change this routing.** Point the vhost at a dead

View file

@ -56,8 +56,8 @@ between "an operator's page" and "anyone with a session."
## What it costs to be reachable
The apex is published to the hive's resolver like every other swarm
service, so agent containers can resolve it. That's deliberate and it's
hyperhive publishes the apex to the hive's resolver like every other
swarm service, so agent containers can resolve it. That's deliberate and it's
not a hole: **reachability isn't the access control here.** An agent
that resolves the name and connects still has no operator session, and
the subrequest denies it.
@ -74,7 +74,7 @@ a container":
| **certificate name** | `nix/host-modules/swarm.nix` (`serviceDomains`) |
The DNS record and the local-dev `/etc/hosts` entry need no separate
edit: both are derived from `services.hyperhive.gateway.localNames`,
edit: both derive from `services.hyperhive.gateway.localNames`,
which a service's own module already has to push its domain into to be
resolvable — see `nix/host-modules/hive-gateway/dnsmasq.nix` and
`.../default.nix`'s `networking.hosts`. `vhosts.nix` itself is scoped to