From fab2a0dedcf98ad94f9bfbe332acfa9f68b1fdaf Mon Sep 17 00:00:00 2001 From: iris Date: Tue, 8 Sep 2026 13:37:57 +0200 Subject: [PATCH] docs: fix genuine passive-voice hits in docs/swarm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- docs/swarm/README.md | 37 +++++++++++++++++++------------------ docs/swarm/ca.md | 24 ++++++++++++------------ docs/swarm/secrets.md | 12 ++++++------ docs/swarm/services.md | 38 +++++++++++++++++++------------------- docs/swarm/sso.md | 40 ++++++++++++++++++++-------------------- docs/swarm/ui.md | 6 +++--- 6 files changed, 79 insertions(+), 78 deletions(-) diff --git a/docs/swarm/README.md b/docs/swarm/README.md index 5a6890d8..71f60c4d 100644 --- a/docs/swarm/README.md +++ b/docs/swarm/README.md @@ -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 diff --git a/docs/swarm/ca.md b/docs/swarm/ca.md index b90aa8d9..9ed6c9f1 100644 --- a/docs/swarm/ca.md +++ b/docs/swarm/ca.md @@ -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 :/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 diff --git a/docs/swarm/secrets.md b/docs/swarm/secrets.md index 6b90532c..4f84864a 100644 --- a/docs/swarm/secrets.md +++ b/docs/swarm/secrets.md @@ -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/.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. diff --git a/docs/swarm/services.md b/docs/swarm/services.md index 98a434ba..8bd8184a 100644 --- a/docs/swarm/services.md +++ b/docs/swarm/services.md @@ -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.` and +The swarm's telemetry lands in one VictoriaMetrics, and one Grafana +reads it, in two containers at `metrics.` and `grafana.`. 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). diff --git a/docs/swarm/sso.md b/docs/swarm/sso.md index 84f269f2..fe17e5b8 100644 --- a/docs/swarm/sso.md +++ b/docs/swarm/sso.md @@ -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: 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 diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index e35d3537..1d1a4d08 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -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