Watch
0
0
Fork
You've already forked hyperhive
0

nix: comments state current behaviour, not history

Rewrite comments and option descriptions under nix/ that narrate how the
code got here ("used to", "new name", "sat at the TOP", "no longer",
renamed/moved narration, attributions) to say what the code does now and
why. Comments and description strings only; no logic changes.

Files in open PR #4970's diff are left alone. Refs #3902
This commit is contained in:
atlas 2026-10-05 02:34:39 +02:00
commit 1c90619fbb
45 changed files with 331 additions and 411 deletions

View file

@ -79,10 +79,10 @@ in
#
# Both spellings, for the same reason ../host-modules/deploy.nix lists both
# of `swarm|deploy.matrix.registrationTokenFile`: an existing agent.nix may
# carry either the pre-rename `hyperhive.*` path or the current one, and only
# the one it actually carries will fire. The rename shim that used to bridge
# them is gone from ./renamed-options.nix — a rename pointing at an
# undeclared option breaks evaluation for everyone, removed or not.
# carry either the `hyperhive.*` path or the `services.hyperhive.agent.*`
# one, and only the one it actually carries will fire. Neither is an entry
# in ./renamed-options.nix — a rename pointing at an undeclared option
# breaks evaluation for everyone, removed or not.
(lib.mkRemovedOptionModule [ "hyperhive" "matrix" "enable" ] matrixEnableRemoved)
(lib.mkRemovedOptionModule [ "services" "hyperhive" "agent" "matrix" "enable" ] matrixEnableRemoved)
];

View file

@ -10,8 +10,8 @@
let
userName = config.services.hyperhive.agent.user.name;
# The token the agent fetched from the swarm secret store
# (./forge-token.nix), then the state-dir file the hive used to write,
# which is still the only copy for an agent without a store identity.
# (./forge-token.nix), then the state-dir file the hive writes, which is
# the only copy for an agent without a store identity.
# Both are PATHS; each reader below takes the first that holds a token.
fetchedTokenFile = config.services.hyperhive.agent.forge.tokenFile;
stateTokenFile = "/agents/${userName}/state/forge-token";

View file

@ -37,7 +37,7 @@ let
# per-agent "does this agent have a homeserver to reach" fact. So an agent the
# hive handed no homeserver URL, whose operator declared no external account
# either, has an empty set here and gets none of the units — the case the
# deleted `matrix.enable = false` used to express.
# "matrix off" case.
#
# No cycle: `matrixAccounts`'s own definition reads `matrix.url`, never this
# binding, so the `mkIf`s below may read the merged option value freely.
@ -75,9 +75,9 @@ in
then talks to the wrong machine. An absent integration, never a
misdirected one.
`null` is also this agent's **matrix off switch**, and the
replacement for the `services.hyperhive.agent.matrix.enable`
boolean that used to exist: the hive-internal `main` account in
`null` is also this agent's **matrix off switch** — there is no
separate `services.hyperhive.agent.matrix.enable` boolean: the
hive-internal `main` account in
`services.hyperhive.agent.matrixAccounts` is declared from this
URL, so `null` leaves that set empty and the whole integration —
daemon unit, path watcher, injected MCP entry — is not generated
@ -214,12 +214,11 @@ in
config = {
assertions = [
# The "extras require `matrix.enable`" assertion that used to head this
# list is gone with the option: a non-empty account set is now what
# enables matrix, so the condition it checked has become the definition
# of the thing it was checking. An operator declaring only an external
# account, with no hive homeserver, is a config this module now renders
# rather than rejects — matrix on, no `main`.
# There is no "extras require `matrix.enable`" assertion: a non-empty
# account set is what enables matrix, so that condition is the
# definition of the thing it would check. An operator declaring only an
# external account, with no hive homeserver, gets a rendered config —
# matrix on, no `main`.
#
# `main` is not a forbidden key --- this module declares it
# itself (see the `matrixAccounts.main` definition below), so the

View file

@ -1,10 +1,9 @@
# Deprecation shims for the agent tier's old option namespace.
# Deprecation shims for the agent tier's top-level `hyperhive.*` spelling.
#
# Every per-agent option used to live at the top level, `hyperhive.<name>`,
# while the host tier has always been under `services.hyperhive.*`. The agent
# tier moved to `services.hyperhive.agent.<name>`; these entries keep an
# existing `agent.nix` evaluating (with a deprecation warning) against the old
# spelling.
# Per-agent options live at `services.hyperhive.agent.<name>`, beside the host
# tier's `services.hyperhive.*`. These entries keep an `agent.nix` that still
# uses the top-level `hyperhive.<name>` spelling evaluating, with a
# deprecation warning.
#
# ⚠️ One entry per LEAF, not per namespace. A single entry for a parent path
# only covers its children when the parent is a submodule-typed option; every
@ -119,9 +118,9 @@
[ "hyperhive" "logs" "queryUrl" ]
[ "services" "hyperhive" "agent" "logs" "queryUrl" ]
)
# `matrix.enable` is deliberately absent from this list: the option it
# renamed to no longer exists, and a rename pointing at an undeclared
# option breaks evaluation for everyone. Both of its spellings are
# `matrix.enable` is deliberately absent from this list: it has no
# replacement option, and a rename pointing at an undeclared option
# breaks evaluation for everyone. Both of its spellings are
# `mkRemovedOptionModule` in ./default.nix instead.
(lib.mkRenamedOptionModule
[ "hyperhive" "matrix" "url" ]

View file

@ -239,8 +239,8 @@ in
# every hive — the name it answers to, the ports and URLs it is reached
# on, the client id it is registered under. These six are what the host
# running it decides; its two packages moved as well, further down with
# the other `*.package` moves. ⚠️ `enable` is NOT here: it was renamed
# earlier and its shim lives in ./hive-matrix.nix, so that file is the one
# the other `*.package` moves. ⚠️ `enable` is NOT here: its shim lives in
# ./hive-matrix.nix, so that file is the one
# to check before assuming this table is the whole matrix story.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "matrix" "openFirewall" ]
@ -255,7 +255,7 @@ in
[ "services" "hyperhive" "deploy" "matrix" "maxRequestSize" ]
)
# Both spellings of the registration token, removed rather than renamed:
# the homeserver no longer accepts a shared registration secret at all,
# the homeserver does not accept a shared registration secret at all,
# so there is no path to point a definition at. Accounts are created by
# the swarm's appservice.
(lib.mkRemovedOptionModule [ "services" "hyperhive" "swarm" "matrix" "registrationTokenFile" ] ''
@ -301,28 +301,27 @@ in
[ "services" "hyperhive" "deploy" "victorialogs" "retentionPeriod" ]
)
# The switch over all of the above, and the name changes with the path
# because the old one described the wrong subject: those services are
# required of the SWARM, while the option says whether THIS host runs
# them. `allSwarmServices` is mara's own phrasing of what it means.
# The switch over all of the above. `allSwarmServices` says whether THIS
# host runs every service the swarm requires; those services are
# required of the SWARM, so the name is about the swarm rather than this
# host's wiring.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "enableRequiredServices" ]
[ "services" "hyperhive" "deploy" "allSwarmServices" ]
)
# The mode above that one. It sat at the TOP of `services.hyperhive`,
# which is the same defect one tier up: that namespace is everything
# about hyperhive, not the settings of a single hive. The new name says
# what the mode asserts — the whole swarm runs on this host — instead of
# naming its mechanism.
# The mode above that one lives under `deploy`, not at the TOP of
# `services.hyperhive`: that namespace is everything about hyperhive, not
# the settings of a single hive. `singleHostSwarm` names what the mode
# asserts — the whole swarm runs on this host — instead of its mechanism.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "enableAllLocalDefaults" ]
[ "services" "hyperhive" "deploy" "singleHostSwarm" ]
)
# The hive TLS state dir sat at the TOP of `services.hyperhive`, which is
# meant to be everything about hyperhive rather than the settings of one
# hive. `hive-controller` is hive-c0re's new name (mara, on the issue).
# The hive TLS state dir lives under `deploy.hive-controller` (the
# hive-c0re daemon's namespace), not at the TOP of `services.hyperhive`,
# which is everything about hyperhive rather than the settings of one hive.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "tls" "stateDir" ]
[ "services" "hyperhive" "deploy" "hive-controller" "tls" "stateDir" ]

View file

@ -86,8 +86,8 @@ in
clientKeyFile = lib.mkDefault "${pkiDir}/client-key.pem";
serverCaFile = lib.mkDefault "${pkiDir}/ca.pem";
# 🩸 Readers that used to present `client.pem` above, each now pointed at
# a leaf of its own. The leaf is what bao sees, so this pairing is the
# 🩸 The readers below each present a leaf of their own rather than
# `client.pem` above. The leaf is what bao sees, so this pairing is the
# whole of what turns "one principal with the union of their grants" into
# one principal per grant — see ./swarm-bao.nix's `perHiveReaders` block
# for the grants themselves.
@ -193,11 +193,11 @@ in
[ -s ${pkiDir}/matrix-ctl.pem ] || ${signLeaf} ${pkiDir} matrix-ctl \
${lib.escapeShellArg deployCfg.bao.matrixCtlCommonName} "" clientAuth
# 🩸 The readers that used to present `client.pem` above. Each carries
# 🩸 The readers' leaves, separate from `client.pem` above. Each carries
# its own subject, which is the entire mechanism: bao matches a
# cert-auth role on the CN, so units sharing one leaf were one principal
# holding the union of their grants, and the union included every agent
# credential in the swarm.
# cert-auth role on the CN, so units sharing one leaf would be one
# principal holding the union of their grants, and the union would
# include every agent credential in the swarm.
#
# ⚠️ The first carries THIS hive's name in the subject, unlike the three
# service leaves above. Its grant names one hive's path, because a queue

View file

@ -42,9 +42,9 @@ let
# a certificate the store accepts, and that is true on the store's own host
# and on a hive three networks away for exactly the same reason.
#
# 🩸 This principal's OWN leaf, not `clientCertFile` — the hive's, which four
# units used to share. Bao matches a cert-auth role on the CN, so one leaf for
# four readers was ONE principal holding the union of four grants: read on
# 🩸 This principal's OWN leaf, not `clientCertFile` — the hive's. Bao
# matches a cert-auth role on the CN, so one leaf shared by four readers
# would be ONE principal holding the union of four grants: read on
# `swarm/agents/*` AND `swarm/hives/<hive>/*` AND `swarm/services/*`, when
# this unit reads one queue credential and nothing else. Its own leaf carries
# `<queueAgentCommonNamePrefix>-<hive>` and its role grants the single path
@ -53,11 +53,11 @@ let
baoDeploy.queueAgentClientCertFile != null && baoDeploy.queueAgentClientKeyFile != null;
# Does this host read the store at all — the hive's own leaf, which is the
# one thing a remote-store deployment has always had to place by hand. Only
# used to decide whether a missing per-principal leaf is a mistake or a
# deployment that has no store: a host holding neither is the supported
# no-store shape, and one holding this pair but not the pair above stopped
# short of naming this reader's own.
# one thing a remote-store deployment has to place by hand. Only used to
# decide whether a missing per-principal leaf is a mistake or a deployment
# that has no store: a host holding neither is the supported no-store
# shape, and one holding this pair but not the pair above reads the store
# without a reader identity of its own.
hiveReaderIdentity = baoDeploy.clientCertFile != null && baoDeploy.clientKeyFile != null;
credentialDir = toString deployCfg.hive-controller.queue.agentCredentialDir;

View file

@ -115,17 +115,14 @@ in
HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES = otel.extraResourceAttributes;
}
# HYPERHIVE_OTEL_HEADERS_CREDENTIAL is deliberately NOT emitted, and
# its absence is the security half of this design: it was the variable
# that put the upstream token into an agent's own settings.json. The
# delivery path it drove — an nspawn credential forwarded by
# host_config.rs, then written to an agent-readable file by
# claude-settings.nix's `hive-otel-header` oneshot — no longer exists
# anywhere; it was removed along with this variable's last consumer.
# its absence is the security half of this design: that variable would
# put the upstream token into an agent's own settings.json. Nothing
# delivers the token into the container — no nspawn credential, no
# agent-readable header file.
#
# Kept as a comment rather than deleted because the useful part is the
# RULE, not the history: the collector holding the credential achieves
# nothing while anything else hands out a copy, so there is exactly one
# holder and it is on the host.
# The rule: the collector holding the credential achieves nothing while
# anything else hands out a copy, so there is exactly one holder and it
# is on the host.
// lib.optionalAttrs (otel.metricIntervalMs != null) {
HYPERHIVE_OTEL_METRIC_INTERVAL_MS = toString otel.metricIntervalMs;
}

View file

@ -207,11 +207,8 @@ in
# `caTrust.containerOrdering` orders this unit after `hive-tls-ca.service`
# in self-signed mode (see the hive-ca-trust helper). Runner registration
# is no longer on the boot-critical path — hive-c0re owns it out of band —
# so the container boots immediately and needs no start-timeout bump. The
# former `TimeoutStartSec = mkForce "180s"` band-aid (which papered over a
# boot-path forge + core-token round-trip that could trip the nspawn start
# timeout into a ~60s restart loop) is gone with that move.
# is not on the boot-critical path — hive-c0re owns it out of band — so
# the container boots immediately and needs no start-timeout bump.
systemd.services."container@hive-ci" = caTrust.containerOrdering;
containers.hive-ci = {
@ -415,7 +412,7 @@ in
# runner stays down (Restart=on-failure retries) until c0re
# registers it, rather than blocking the container's start.
#
# No nix-daemon wait anymore: builds route through the HOST daemon
# No nix-daemon wait: builds route through the HOST daemon
# (bound socket above), which is up before the container — nix's
# own connect retries cover any brief window. `mkBefore` runs this
# ahead of any upstream pre-steps.

View file

@ -142,11 +142,6 @@ in
matter for access from outside the host. Flip to `true` when
you want the operator's browser or external git clients to
hit the forge directly.
**Breaking change**: this used to default to `true`. If you
relied on the old default for external reach, add
`services.hyperhive.deploy.forgejo.openFirewall = true;` to your
host config before rebuilding.
'';
};
@ -783,7 +778,7 @@ in
Group = "forgejo";
SyslogIdentifier = "forgejo-sso-source";
};
# `FORGEJO_CUSTOM` (not `GITEA_CUSTOM` — forgejo renamed it)
# `FORGEJO_CUSTOM` (the forgejo-prefixed name, not `GITEA_CUSTOM`)
# is how the CLI finds the app.ini upstream's module wrote.
#
# This unit is also a trust-bundle consumer (declared in the
@ -930,29 +925,25 @@ in
# re-mint instead of being silently masked by a stamp that
# outlived what it claims exists.
if [ ! -s "$token_path" ]; then
# `write:admin` added for `swarm-controller::forge::Client::
# ensure_agent_user` (the `CreateForgeUser` jobq node) —
# Forgejo's `admin_create_user` HTTP endpoint refused every
# call with "token does not have at least one of required
# scope(s): [write:admin]" without it, an already-deployed
# swarm hitting this the moment that node shipped. A host
# whose token was minted before this scope was added won't
# pick it up automatically — the `[ ! -s "$token_path" ]`
# guard above only mints when the file is absent, by design
# (see its own comment) — so an existing deployment needs
# its token file deleted to force a re-mint with the new
# scope.
# `write:admin` is required by `swarm-controller::forge::Client::
# ensure_agent_user` (the `CreateForgeUser` jobq node):
# Forgejo's `admin_create_user` HTTP endpoint refuses a
# token without it ("token does not have at least one of
# required scope(s): [write:admin]"). A host whose token was
# minted without this scope keeps it — the
# `[ ! -s "$token_path" ]` guard above only mints when the
# file is absent, by design (see its own comment) — so such a
# deployment needs its token file deleted to force a re-mint
# with the full scope.
#
# Token name carries a timestamp suffix rather than the
# fixed `swarm-controller-boot` this used to be — a real
# incident, not a hypothetical: deleting the token *file*
# (the remediation above) doesn't delete the token Forgejo
# already has under that name, so re-running this unit hit
# "Command error: access token name has been used already"
# and failed outright. Same fix `hive-c0re::forge::users::
# mint_token` already uses for its own agent tokens, for
# the identical reason — a monotonic suffix means a re-mint
# can never collide with whatever this account already has.
# Token name carries a timestamp suffix rather than a fixed
# name: deleting the token *file* (the remediation above)
# doesn't delete the token Forgejo already has under that
# name, so re-running this unit would fail with "Command
# error: access token name has been used already". Same
# approach `hive-c0re::forge::users::mint_token` uses for its
# own agent tokens — a monotonic suffix means a re-mint can
# never collide with whatever this account already has.
out=$(forgejo admin user generate-access-token \
--username ${lib.escapeShellArg swarmControllerForgeUser} \
--token-name "swarm-controller-boot-$(date +%s)" \

View file

@ -30,9 +30,10 @@ let
effectiveRootUrl = if cfg.rootUrl != null then cfg.rootUrl else defaultRootUrl;
in
{
# Forge moved under `swarm` when the swarm-global services were
# consolidated. One rename for the namespace: the subtree comes with it,
# so existing hives keep evaluating and get one warning naming both paths.
# Forge lives under `swarm`, beside the other swarm-global services. One
# rename for the top-level `services.hyperhive.forge` namespace: the
# subtree comes with it, so existing hives keep evaluating and get one
# warning naming both paths.
imports = [
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "forge" ]

View file

@ -55,10 +55,9 @@ let
# `cfg.useSelfSigned` (options.nix) is the derived single source of truth.
useSelfSigned = cfg.useSelfSigned;
# nginx's own state dir. Kept at the historical `/var/lib/hive-gateway`
# path rather than renamed with the move: it holds the imported leaf
# across reboots, and renaming it would strand every existing hive's
# certs for no gain.
# nginx's own state dir, fixed at `/var/lib/hive-gateway` independent of
# this module's own path: it holds the imported leaf across reboots, and
# a different path would strand every existing hive's certs.
tlsDir = "/var/lib/hive-gateway/tls";
# TLS cert + key paths.
# - self-signed (default): the hive leaf the store issues (the
@ -207,14 +206,12 @@ in
# before c0re has run, and pin owner + mode rather than leaving it
# to whoever creates the path first.
#
# ⚠️ There is deliberately NO rule for /var/lib/hyperhive here. One
# used to declare it `0755 root root` and could never win:
# ⚠️ There is deliberately NO rule for /var/lib/hyperhive here.
# `hive-c0re.service` sets `StateDirectory = "hyperhive"` with
# `StateDirectoryMode = "0750"`, which systemd re-applies on every
# start. Two mechanisms owning one path, and the loser still read as
# a guarantee — it is why the resulting outage was first misdiagnosed
# as someone having changed the mode. c0re's own unit owns that dir;
# this module no longer has an opinion about it.
# start, so a tmpfiles rule for the same path could never win: two
# mechanisms owning one path, the loser still reading as a guarantee.
# c0re's own unit owns that dir; this module has no opinion about it.
systemd.tmpfiles.rules = [
# The gateway's own config dir — NOT under /var/lib/hyperhive. c0re
# writes here, nginx reads here, and neither needs any access to the
@ -393,23 +390,15 @@ in
# ⚠️ NO `SupplementaryGroups = [ "hive-core" ]` on nginx, and its
# absence is load-bearing rather than an omission.
#
# It used to be here, to let nginx traverse `/var/lib/hyperhive` and
# reach the config fragments that lived inside: `hive-c0re.service`
# declares `StateDirectory = "hyperhive"` with `StateDirectoryMode =
# "0750"` owned by `hive-core`, and systemd re-applies that on every
# c0re start — beating this module's own tmpfiles rule for the same
# path. Without the group, nginx failed its config test and never
# started (`nginx: [emerg] open() ".../agents.conf" failed (13:
# Permission denied)`), taking the gateway and every hive domain
# behind it down.
#
# The group fixed the symptom and paid for it: it also gave nginx
# read access to everything ELSE group-readable under that dir,
# including the broker sqlite — i.e. every message between every
# agent, reachable by the process whose entire job is parsing
# untrusted network input. Moving the fragments to the gateway's own
# dir removes the need and the exposure together. Re-adding this line
# would restore both.
# `hive-c0re.service` declares `StateDirectory = "hyperhive"` with
# `StateDirectoryMode = "0750"` owned by `hive-core`, and systemd
# re-applies that on every c0re start. The group would let nginx
# traverse that dir, but it also gives nginx read access to everything
# ELSE group-readable under it, including the broker sqlite — i.e.
# every message between every agent, reachable by the process whose
# entire job is parsing untrusted network input. The config fragments
# nginx reads live in the gateway's own dir instead, so nginx needs no
# access to c0re's tree. Adding this line would reopen that exposure.
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [
@ -424,12 +413,11 @@ in
# every name a service module contributed. See `docs/networking/gateway.md`
# ("Local dev").
#
# This used to restate the per-service list a THIRD time (after the
# vhosts and the dnsmasq records), with its own copy of each
# service's guard. It is the same question — "which names does this
# host answer for" — so it reads the same answer; a service added
# later lands here with no edit, and cannot land here with a
# different condition than it used for DNS.
# This does not restate the per-service list (the vhosts and the
# dnsmasq records already carry it). It is the same question —
# "which names does this host answer for" — so it reads the same
# answer; a service added later lands here with no edit, and cannot
# land here with a different condition than it used for DNS.
networking.hosts = lib.mkIf cfg.localHostsEntry {
"127.0.0.1" = lib.unique ([ hyperhiveDomain ] ++ cfg.localNames);
};

View file

@ -82,10 +82,6 @@ in
nginx vhost. Leave off when running behind another reverse
proxy (e.g. caddy / traefik on the host) that handles TLS
termination + forwards to `port`.
**Note**: this used to default to `true`. Add
`services.hyperhive.gateway.openFirewall = true;` to your host
config if external reach stopped working after a recent upgrade.
'';
};

View file

@ -187,22 +187,19 @@ let
# Swagger UI: nginx hosts the FULL themed dist (`swaggerUiTheme` —
# vendored Swagger UI + our overlay, see nix/packages/swagger-ui-
# dist.nix + swagger-ui-theme.nix) straight from the store, with NO
# fallback to hive-c0re at all — per the operator's shape: "core
# should not need the swagger ui at all if it is hosted in gateway"
# / "core only hosts the json". Only `/api/openapi.json`
# (the live-generated spec `index.html` fetches; not under this
# prefix) still proxies to c0re via "/api/" below — that's the one
# thing that has to stay dynamic.
# fallback to hive-c0re at all: core hosts only the JSON. Only
# `/api/openapi.json` (the live-generated spec `index.html` fetches;
# not under this prefix) proxies to c0re via "/api/" below — that's
# the one thing that has to stay dynamic.
#
# Prefix location, not exact-match: wins over "/api/" on plain
# prefix length (no ordering/`=` needed), and now needs to cover
# every file in the tree (bundle.js, maps, favicons, …), not just
# our 2 override files — hive-c0re no longer serves any of this as
# a fallback once its own `utoipa-swagger-ui` mount is removed.
# `= /api/docs` (no trailing slash) issues the same redirect
# `utoipa-swagger-ui`'s router used to: that mount is going away
# too, so nginx has to own it now, or the H0M3 hub's own `/api/docs`
# link (no trailing slash) would 404 once hive-c0re drops the route.
# prefix length (no ordering/`=` needed), and covers every file in
# the tree (bundle.js, maps, favicons, …), not just our 2 override
# files — hive-c0re serves none of this as a fallback.
# `= /api/docs` (no trailing slash) redirects to `/api/docs/`: nginx
# owns that redirect because hive-c0re has no route for it, and the
# H0M3 hub's own `/api/docs` link (no trailing slash) would 404
# without it.
swaggerUiLocations = {
"= /api/docs" = {
extraConfig = ''

View file

@ -153,12 +153,11 @@ let
# to host it — otherwise moving the container between hives would look
# like a different homeserver.
#
# ⚠️ Changing this default is a BREAKING change in a way that moving
# `gatewayHost` was not: `serverName` is baked irrevocably into every
# user and room id, so a deployment that rebuilds onto a new one is a
# *different homeserver*, not a renamed one. Existing hives pin the old
# value explicitly (see the option's description); the default is what
# a fresh swarm gets.
# ⚠️ Changing this default is breaking: `serverName` is baked
# irrevocably into every user and room id, so a deployment that
# rebuilds onto a new one is a *different homeserver*. Existing hives
# pin their value explicitly (see the option's description); the
# default is what a fresh swarm gets.
#
# Total on a null swarm domain, deliberately: the required-domain
# assertion in hive-network.nix is what should fire, not a coercion
@ -251,9 +250,10 @@ in
# settings: docs/integrations/matrix.md. Vhost map + discovery flow + tuning
# knobs: docs/networking/gateway.md.
# Matrix moved under `swarm` when the swarm-global services were
# consolidated. One rename for the namespace: the subtree comes with it,
# so existing hives keep evaluating and get one warning naming both paths.
# Matrix lives under `swarm`, beside the other swarm-global services. One
# rename for the top-level `services.hyperhive.matrix` namespace: the
# subtree comes with it, so existing hives keep evaluating and get one
# warning naming both paths.
imports = [
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "matrix" ]
@ -311,11 +311,6 @@ in
the homeserver to other hives or when an external matrix
client needs to reach the client-server API directly.
**Breaking change**: this used to default to `true`. If you
relied on the old default for external reach, add
`services.hyperhive.deploy.matrix.openFirewall = true;` to your host
config before rebuilding.
Note: federation (the matrix-spec well-known port 8448) is
intentionally not opened here. tuwunel serves the federation
API on the same `httpPort` as the client-server API by

View file

@ -130,9 +130,9 @@ let
# leaf to hold, and its absence there is the correct state rather
# than a stale one — its own vhosts are covered by the hive leaf
# above. Anywhere else it is expected: the leaf comes from the
# secret store's `pki` mount now, not from a sub-CA that exists on
# one host in the swarm, so "this host cannot issue it" is no longer
# one of the answers.
# secret store's `pki` mount, reachable from every host rather than
# from a sub-CA on one host in the swarm, so "this host cannot issue
# it" is not one of the answers.
want_svc=${if localServiceDomains == [ ] then "0" else "1"}
# Expiry is not the only way a leaf goes wrong. One signed when the
@ -346,17 +346,14 @@ in
};
config = lib.mkIf active {
# 🚫 The "these service names are outside this hive's domain and this
# host cannot sign for them" warning is GONE, and its absence is the
# deliverable rather than a tidy-up. It fired on
# `!swarm.ca.autoConfigure` — i.e. "this host does not hold the swarm
# root key" — which was the whole reason a hive could end up serving
# its own leaf on a swarm-service name. The services leaf now comes
# from the secret store's `pki` mount, which every hive reaches over
# the network with its own identity, so holding the root key stopped
# being the thing that decides. Left in place it would warn, on every
# rebuild of every hive that is not the CA host, about a fallback
# that no longer happens.
# 🚫 There is deliberately NO warning that these service names are
# outside this hive's domain and this host cannot sign for them. The
# services leaf comes from the secret store's `pki` mount, which every
# hive reaches over the network with its own identity, so holding the
# swarm root key does not decide whether a hive can serve a
# swarm-service name. Such a warning would fire on every rebuild of
# every hive that is not the CA host, about a fallback that does not
# happen.
# The state dir and the trust bundle. Never waits on the store: the
# store's container is ordered after this unit, which writes the

View file

@ -103,8 +103,8 @@ in
# Where the swarm lives. Declared beside the hive's own identity
# because it is what that identity is derived FROM — every hive in a
# swarm is a sub-domain of it. Unlike the renamed options nearby, this
# is genuinely new: nothing moved here, so there is no alias.
# swarm is a sub-domain of it. No alias: this option has no earlier
# spelling.
options.services.hyperhive.swarm.domain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
@ -145,7 +145,7 @@ in
(`services.hyperhive.deploy.hive-controller.enable`,
`deploy.bao.enable`, `deploy.matrix.enable`). Distinct from
`services.hyperhive.domain` (the machine-addressable DNS name)
but no longer merely cosmetic: a hive occupies
and not merely cosmetic: a hive occupies
`<hiveName>.<swarm.domain>`, so this is the label the hive is
*addressed* by as well as the one it is called. Exposed to
agents as `HYPERHIVE_HIVE_NAME`; surfaced in the dashboard

View file

@ -60,9 +60,8 @@ in
# rather than the file.
#
# ⛔ **Do not hand this path to a consumer that reads only ONE
# certificate from it.** This comment used to offer an OIDC verifier's
# `issuer_ca_path` as the motivating example; that is exactly what took
# the swarm collector down for forty minutes once. The bundle is
# certificate from it** — an OIDC verifier's `issuer_ca_path` is
# exactly such a reader, and takes the swarm collector down. The bundle is
# `system CAs ++ hive anchors`, so the anchor is ~123rd, and a
# first-certificate-only reader gets whichever public CA sorts first and
# can verify nothing of ours — with a full, valid, 125-certificate file

View file

@ -68,7 +68,7 @@ in
⚠️ The collector is therefore in the path of all telemetry. It runs
on the same host as the agents and restarts on failure, and
telemetry is not the control plane, so degraded telemetry is not
degraded operation — but the export no longer survives independently
degraded operation — but the export does not survive independently
of anything host-side
'';
@ -239,7 +239,7 @@ in
onward, which is one hop for the whole hive rather than one per
target.
No longer empty on a default hive: this collector's own endpoint
Not empty on a default hive: this collector's own endpoint
is always in here, so the scrape receiver is emitted everywhere.
The `!= { }` guards around it stay meaningful — the set can still
be forced empty, and a `prometheus` receiver with nothing to

View file

@ -132,10 +132,10 @@ in
tracked as a follow-up rather than papered over.
On by default: a swarm's hives have identities, and that is a
fact about the swarm rather than about any one host. It used to
default to whether the queue ran on *this* machine, which made
the answer differ between two hosts of one swarm — set it false
for a swarm whose hives authenticate to nothing.
fact about the swarm rather than about any one host. It does not
depend on whether the queue runs on *this* machine, so the answer
is the same on every host of one swarm — set it false for a swarm
whose hives authenticate to nothing.
The clients are inert until something authenticates with them:
each is a client id and a secret sitting on this host. What

View file

@ -171,7 +171,7 @@ let
# 🏷️ The id EXTENDS the hive's own (`hive-<name>-agent`) rather than taking a
# prefix of its own (`agent-<name>`), because that reads as *the agent called
# `<name>`* — which is the one thing this identity does not carry. The cost is
# that the two ids are no longer distinguishable by prefix, so the responder's
# that the two ids are not distinguishable by prefix, so the responder's
# agent rule must be tried BEFORE its hive rule; `policy.rs` says so at the
# match site, and the assertion below covers the case that ordering cannot.
#
@ -905,11 +905,11 @@ in
};
environment = {
SWARM_AUTHELIA_BRIDGE_BIND = "127.0.0.1:${toString cfg.bridgePort}";
# ONE file, and this is it. The bridge used to carry a
# second, private `…-users.json` it treated as canonical
# while writing `users.yml` as a rendering of it — two
# canonical stores for one physical file, which is what
# made `swarm agent create` refuse to start on a hive whose
# ONE file, and this is it. The bridge keeps no second,
# private users store: a store of its own treated as canonical,
# with `users.yml` written as a rendering of it, would be two
# canonical stores for one physical file, and would make
# `swarm agent create` refuse to start on a hive whose
# `users.yml` already held users.
SWARM_AUTHELIA_BRIDGE_USERS_FILE = deployCfg.authelia.usersFile;
# The CONFIGURED authelia, not whatever is on `PATH`: the

View file

@ -760,22 +760,22 @@ let
}
'';
# ── the principals that used to share the hive's own leaf ──────────────────
# ── the principals that each hold a leaf of their own ──────────────────────
#
# 🩸 Each of these reads exactly ONE path in the store, and until this split
# each did it holding `deploy.bao.clientCertFile` — the hive's own leaf,
# whose policy (`swarm-secret-client`'s `policy::render`) granted read on
# `swarm/agents/*`, `swarm/hives/<hive>/*` AND `swarm/services/*`. Principals
# presenting one certificate are ONE principal to bao, so the union of what
# they need was the only grant expressible: the unit that fetches Grafana's
# OIDC client secret could fetch every agent credential in the swarm, and the
# unit that fetches this hive's queue credential could fetch Grafana's. Least
# privilege was not misconfigured, it was unrepresentable.
# 🩸 Each of these reads exactly ONE path in the store, and does it holding
# its own leaf rather than `deploy.bao.clientCertFile` — the hive's own leaf,
# whose policy (`swarm-secret-client`'s `policy::render`) grants read on
# `swarm/agents/*`, `swarm/hives/<hive>/*` AND `swarm/services/*`.
# Principals presenting one certificate are ONE principal to bao, so sharing
# the hive's leaf would leave only the union of what they need expressible:
# the unit that fetches Grafana's OIDC client secret could fetch every agent
# credential in the swarm, and the unit that fetches this hive's queue
# credential could fetch Grafana's.
#
# Each now gets its own leaf, its own cert-auth role and a policy holding the
# one `secret/data/…` path that principal's script actually names — spelled to
# the LEAF rather than to a prefix, for the reason `matrixCtlPolicyText` above
# gives for doing the same.
# Each gets its own leaf, its own cert-auth role and a policy holding the
# one `secret/data/…` path that principal's script actually names — spelled
# to the LEAF rather than to a prefix, for the reason `matrixCtlPolicyText`
# above gives for doing the same.
#
# ⚠️ The queue-credential reader is PER HIVE and the OIDC readers are not,
# and that asymmetry is the shape of the paths rather than a preference. A
@ -784,7 +784,7 @@ let
# `swarm/services/<client-id>/` and a swarm registers each of those exactly
# once. A single role for a per-hive reader would have to name `hives/*` to
# serve every hive — which would let one hive read another's queue
# credential.
# credential, a reach no hive needs.
readStanza = path: ''
path "${path}" {
capabilities = ["read"]
@ -1024,10 +1024,9 @@ let
# `tls_require_and_verify_client_cert` on the API listener to scrape THAT
# instead, would each trade that property away for nothing.
#
# Unconditional, and that is the change this carries: the reader is now the
# collector in this very container (below), which renders wherever the store
# does. There is no longer a host where this endpoint has no reader, so there
# is nothing left for it to be a function of.
# Unconditional: the reader is the collector in this very container (below),
# which renders wherever the store does. There is no host where this
# endpoint has no reader, so it is not a function of anything.
metricsListener = {
metrics = {
type = "tcp";
@ -1104,22 +1103,18 @@ let
'';
# ⚠️ Neither the listener above nor the forwarder below has a condition of
# its own, and they lost theirs for the same reason at different times.
# its own.
#
# The listener used to be gated on `scrapeHere = deployCfg.swarm-otel.enable`
# — "does the swarm's collector happen to run on THIS host" — because the
# thing reading it was that collector, over loopback, which only works when
# the two land together. mara's ruling (*"move the existing scraper
# to the local collector"*) removes the premise rather than the symptom: the
# scraper is now in this container, so the reader is wherever the store is,
# and the metrics path stops being silently dead on every host that does not
# also run swarm-otel.
# The listener's reader is the collector in this container, which sits
# wherever the store does. "Does the swarm's collector run on THIS host" is
# not a question it depends on: gating it on that would leave the metrics
# path silently dead on every host that does not also run swarm-otel.
#
# The forwarder never had a defensible one. "Does the swarm have a collector"
# is not a question that can be false (mara: "the swarm always has at least
# one instance of all the swarm level services"), so both earlier gates —
# `otel.enable`, then `deploy.swarm-otel.enable` — asked about the wrong host
# and rendered no forwarder wherever the answer happened to be no.
# "Does the swarm have a collector" is not a question that can be false —
# the swarm always has at least one instance of all the swarm level
# services — so gating the forwarder on `otel.enable` or
# `deploy.swarm-otel.enable` would ask about the wrong host and render no
# forwarder wherever the answer happened to be no.
#
# The only condition that belongs on either is the one already enclosing this
# block: the store is deployed on this host.
@ -2133,14 +2128,13 @@ in
# granter step, on the host where that step runs.
environment.etc."hyperhive/bao-bootstrap-policy.hcl".source = ./bao-bootstrap-policy.hcl;
# 🚫 No `swarm.otel.scrapeTargets.bao` entry any more, and its absence is
# the deliverable rather than a tidy-up. That option is read only by the
# SWARM collector, over loopback, so declaring the store there meant its
# metrics existed exactly on the hosts that also ran swarm-otel and
# nowhere else — silently, since a store with no entry looks identical to
# one nothing scrapes. The scrape moved into this container's own
# collector (see `receivers.prometheus` below), which travels with the
# container's own collector is confirmed.
# 🚫 No `swarm.otel.scrapeTargets.bao` entry, and its absence is
# deliberate. That option is read only by the SWARM collector, over
# loopback, so a store declared there would have metrics exactly on the
# hosts that also run swarm-otel and nowhere else — silently, since a
# store with no entry looks identical to one nothing scrapes. The scrape
# runs in this container's own collector (see `receivers.prometheus`
# below), which travels with the store.
# ⚠️ The one nginx exception to this file's header, and it is one because
# it never terminates. `ssl_preread` reads the SNI off the ClientHello
@ -2898,11 +2892,11 @@ in
'';
};
# 🩸 The principals that used to log in as the hive. See the block around
# `perHiveReaders` for what each grant is and why the first is per hive;
# the units are identical apart from the objects they
# write, so the shape lives in `readerPolicyUnit` and the difference is
# visible here in one line each.
# 🩸 The principals that each log in with a leaf of their own, not as the
# hive. See the block around `perHiveReaders` for what each grant is and
# why the first is per hive; the units are identical apart from the
# objects they write, so the shape lives in `readerPolicyUnit` and the
# difference is visible here in one line each.
systemd.services.swarm-bao-queue-agent-policy = readerPolicyUnit "write the per-hive queue-credential bao policies and cert-auth roles" queueAgentReaders;
systemd.services.swarm-bao-grafana-oidc-policy = readerPolicyUnit "write Grafana's OIDC-secret-reader bao policy and cert-auth role" grafanaOidcReaders;
@ -3701,31 +3695,26 @@ in
# this line leaves a collector that validates, starts,
# reports healthy and forwards nothing.
directory = "/var/log/journal";
# ⚠️ `--merge` stays from when this journal was host-linked
# and `journalctl --follow` (unqualified) found no entry for
# the current boot under that tree, crash-looping the
# receiver every ~2s with "No journal boot entry found for
# the specified boot (+0)". The journal is local now, so the
# current boot has an entry here too — kept rather than
# dropped so the first deploy changes exactly one thing;
# revisit once delivery is confirmed.
# ⚠️ `--merge` is a guard: an unqualified
# `journalctl --follow` over a journal with no entry for the
# current boot (a host-linked one) crash-loops the receiver
# every ~2s with "No journal boot entry found for the
# specified boot (+0)". This journal is container-local, so
# the current boot has an entry and the flag changes nothing.
merge = true;
storage = "file_storage";
};
# The scrape that used to belong to the swarm collector, now
# one hop shorter and one host less fussy — mara:
# *"move the existing scraper to the local collector"*.
# The store's scrape, run by this container's own collector.
#
# ⚠️ `metrics_path` and `params` are STATED, and both have to
# be: openbao serves no `/metrics` at all, and
# `/v1/sys/metrics` answers JSON unless the format is asked
# for. A scrape of the default path 404s and a scrape without
# the query parses as nothing — each reads as a dead exporter
# rather than as a wrong request, which is how the previous
# owner of this scrape learned to spell it
# (./swarm-otel.nix's `loopbackScrapeConfig`, still the
# renderer for every other service's target).
# rather than as a wrong request. The spelling matches
# ./swarm-otel.nix's `loopbackScrapeConfig`, the renderer for
# every other service's target.
#
# No credential, unlike the swarm collector's
# `publishedScrapeTargets` jobs: the target is the loopback

View file

@ -15,12 +15,11 @@
# something real — nothing whose trust store is built at build time can
# name it: `docs/swarm/ca.md::Distributing the root`.
#
# ⚠️ This root no longer issues the swarm's SERVICE certificates, and the
# `swarm-services-ca` sub-CA that used to sit under it is gone rather than
# moved. Those come out of the secret store's own `pki` mount now, anchored
# on a root generated inside it (./swarm-bao.nix), and ./hive-tls.nix asks
# for them over the network instead of signing them here. What is left here
# is one job: the anchor each hive's own CA is issued under.
# ⚠️ This root does not issue the swarm's SERVICE certificates; there is no
# `swarm-services-ca` sub-CA under it. Those come out of the secret store's
# own `pki` mount, anchored on a root generated inside it (./swarm-bao.nix),
# and ./hive-tls.nix asks for them over the network instead of signing them
# here. This root has one job: the anchor each hive's own CA is issued under.
{
lib,
config,

View file

@ -336,8 +336,8 @@ in
hostUnit = true;
enable = deployCfg.swarm-controller.enable;
})
# The avatar moved with the forge-objects pass that uploads it: from
# hive-c0re's boot sweep to this daemon.
# The avatar belongs to the forge-objects pass that uploads it, which
# runs in this daemon.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "c0re" "orgAvatarPng" ]
[ "services" "hyperhive" "deploy" "swarm-controller" "configOrgAvatarPng" ]

View file

@ -215,11 +215,11 @@ let
# its identity yet, which is a thing to say out loud rather than to route
# around by reaching into authelia's tree whenever it happens to be local.
#
# 🩸 Grafana's OWN leaf, not `clientCertFile` — the hive's, which four units
# used to share. Bao matches a cert-auth role on the CN, so one leaf for four
# readers was ONE principal holding the union of four grants: this unit could
# read every agent credential in the swarm and every other service's OIDC
# client secret, when what it needs is the one path `storeSecretPath` names.
# 🩸 Grafana's OWN leaf, not `clientCertFile` — the hive's. Bao matches a
# cert-auth role on the CN, so one leaf shared by four readers would be ONE
# principal holding the union of four grants: this unit could read every
# agent credential in the swarm and every other service's OIDC client
# secret, when what it needs is the one path `storeSecretPath` names.
haveClientIdentity =
baoDeploy.grafanaOidcClientCertFile != null && baoDeploy.grafanaOidcClientKeyFile != null;
@ -377,7 +377,7 @@ in
and for two different reasons. Traces and Profiles front Tempo
and Pyroscope, which this swarm does not run — a UI for a backend
that does not answer. Logs Drilldown is the subtler one: this
swarm *does* run a log store, so that argument no longer applies
swarm *does* run a log store, so that argument does not apply
to it, but the app is Loki-only and no configuration here changes
that. Its volume views call `/loki/api/v1/index/volume`, which
VictoriaLogs does not implement and answers "unsupported path
@ -398,12 +398,12 @@ in
services.hyperhive.gateway.enable = lib.mkDefault true;
services.hyperhive.gateway.dns.enable = lib.mkDefault true;
# Registering the client is NOT here any more: it has to happen on the
# host that runs authelia, and this whole block is gated on the host that
# runs Grafana. ./glue-grafana-oidc-client.nix is where it moved to.
# Registering the client is NOT here: it has to happen on the host that
# runs authelia, and this whole block is gated on the host that runs
# Grafana. ./glue-grafana-oidc-client.nix does it.
# This arm fires only where Grafana runs — this whole block is under
# `deploy.grafana.enable`. SSO itself needs no assertion any more:
# `deploy.grafana.enable`. SSO itself needs no assertion:
# `swarm.authelia.url` is non-nullable, so every swarm has one and the
# OIDC block below always names a real endpoint.
assertions = [
@ -511,14 +511,13 @@ in
# authelia's host wrote it into, and that is true whether authelia is a
# network away or in the container next door.
#
# 🩸 A second unit here used to copy the plaintext out of authelia's host
# tree wherever the two were co-located, with the OIDC block gated on
# either route being available. The ruling that deleted it: the store
# exists so a host holds ONE out-of-band secret — its client certificate —
# and reads everything else with it, so skipping the store when the
# producer is local buys a round trip and costs a second delivery path, a
# second way for the file to be wrong, and a gate whose false arm was a
# Grafana with no login. Recorded in docs/swarm/secrets.md.
# 🩸 No second unit copies the plaintext out of authelia's host tree
# where the two are co-located. The store exists so a host holds ONE
# out-of-band secret — its client certificate — and reads everything else
# with it, so skipping the store when the producer is local would buy a
# round trip and cost a second delivery path, a second way for the file
# to be wrong, and a gate whose false arm is a Grafana with no login.
# Recorded in docs/swarm/secrets.md.
#
# Shaped after ./glue-queue-agent-credential.nix, because it is a store
# reader and this is what the store's readers do: a cert login that fails
@ -771,18 +770,17 @@ in
# property this option exists for — it should not arrive only
# once a plugin happens to be listed.
#
# - 🩸 and the one this comment used to miss: it also sets
# upstream's `plugins.preinstall_disabled` (nixpkgs'
# - 🩸 it also sets upstream's `plugins.preinstall_disabled`
# (nixpkgs'
# grafana.nix defaults it to `declarativePlugins != null`,
# saying preinstall "causes issues with declarativePlugins").
# Preinstall is what fetches the Drilldown apps on first
# start, so claiming this directory silently removes them.
# That is why the option's default is not empty.
#
# The claim that used to sit here — "nothing is taken over on a
# hive with no plugins, manual installation is impossible anyway"
# — reasoned only about plugins a PERSON installs. Upstream
# installs some itself, and those are the ones that went missing.
# Taking this directory over is not harmless on a hive with no
# plugins: that reasoning covers only plugins a PERSON installs,
# and upstream installs some itself.
declarativePlugins = deployCfg.grafana.plugins;
settings = {
@ -839,9 +837,7 @@ in
# Not conditional on anything, and the block below is not
# either, which is what keeps the two from ever disagreeing.
# Whether a password box sits on a public login page cannot
# depend on which host happens to run authelia — and it used to,
# in a module where the OIDC block was gated and this line was
# not.
# depend on which host happens to run authelia.
auth.disable_login_form = true;
# Emitted in every deployment, because SSO is a requirement of

View file

@ -115,10 +115,9 @@ in
today) listens on, at `127.0.0.1`. Reached through the gateway at
`https://''${domain}/''${producerName}/`, same shape as a hive's
own receiver — a swarm-level producer may not share a host with
this collector (mara, on the swarm-controller topology: "the
swarm services dont have to run on the same host as the swarm
controller"), so loopback-only reachability is not a supported
shape here any more than it is for a hive.
this collector (the swarm services need not run on the same host
as the swarm controller), so loopback-only reachability is not a
supported shape here, as it is not for a hive.
Authenticated by `oidc/''${producerName}`, checking for the
audience `services.hyperhive.swarm.controller.queueClientId`

View file

@ -250,10 +250,10 @@ let
# IS asserted is narrower and lives in `assertions` below — see
# `hiveReaderIdentity`.
#
# 🩸 The collector's OWN leaf, not `clientCertFile` — the hive's, which four
# units used to share. Bao matches a cert-auth role on the CN, so one leaf for
# four readers was ONE principal holding the union of four grants: this unit
# could read every agent credential in the swarm and Grafana's OIDC client
# 🩸 The collector's OWN leaf, not `clientCertFile` — the hive's. Bao
# matches a cert-auth role on the CN, so one leaf shared by four readers
# would be ONE principal holding the union of four grants: this unit could
# read every agent credential in the swarm and Grafana's OIDC client
# secret, when what it needs is the one path `storeSecretPath` names.
haveClientIdentity =
baoDeploy.otelOidcClientCertFile != null && baoDeploy.otelOidcClientKeyFile != null;
@ -396,23 +396,21 @@ in
# this is how it authenticates *itself* to a service published behind
# the gateway.
#
# Registering the client is NOT here any more: it has to happen on the
# host that runs authelia, and this whole block is gated on the host that
# runs the collector. ./glue-swarm-otel-oidc-client.nix is where it moved
# to, the same split ./swarm-grafana.nix made for its own client.
# Registering the client is NOT here: it has to happen on the host that
# runs authelia, and this whole block is gated on the host that runs the
# collector. ./glue-swarm-otel-oidc-client.nix does it, the same split
# ./swarm-grafana.nix has for its own client.
# THE delivery unit — one route, in every deployment. The secret authelia
# minted arrives out of the swarm secret store, which the publisher on
# authelia's host wrote it into, whether authelia is a network away or in
# the container next door.
#
# 🩸 A unit here used to copy the plaintext directly out of authelia's
# host tree, reachable only because they share this host's network
# namespace — which stopped working the moment authelia moved to another
# host, which is the defect this whole change exists to fix. The ruling
# that deleted it rather than gave it a remote sibling: the store exists
# so a host holds ONE out-of-band secret — its client certificate — and
# reads everything else with it. Recorded in docs/swarm/secrets.md.
# 🩸 No unit here copies the plaintext directly out of authelia's host
# tree: that is reachable only where authelia shares this host's network
# namespace, and breaks the moment authelia is on another host. The store
# exists so a host holds ONE out-of-band secret — its client certificate —
# and reads everything else with it. Recorded in docs/swarm/secrets.md.
#
# Shaped after ./glue-queue-agent-credential.nix: a cert login that fails
# LOUDLY, since every state it fails on is one a retry fixes, then a read
@ -638,8 +636,8 @@ in
'';
}
{
# The blacklist now lives in a shared file, so this module no longer
# controls its contents — and a guard whose subject can be edited
# The blacklist lives in a shared file, so this module does not
# control its contents — and a guard whose subject can be edited
# elsewhere has to assert that its own case is still in there. Without
# this, deleting one line from `reserved-names.nix` would silently
# retire the check below rather than fail anything.

View file

@ -1,10 +1,10 @@
# Migration shim for `services.hyperhive.swarm.peers`, replaced by the
# `swarm.hives` directory in ./swarm.nix.
#
# ⚠️ This could not be a `mkRenamedOptionModule`. `hives` is not `peers`
# under a new name, it is **`peers` ∪ {self}**: the set gains a member no
# ⚠️ This is not a `mkRenamedOptionModule`. `hives` is not `peers`
# under another name, it is **`peers` ∪ {self}**: the set gains a member no
# existing config has ever written down, because a host's own identity
# lived in *other* options entirely. A rename migrates a name and a
# lives in *other* options entirely. A rename migrates a name and a
# default can re-root a meaning; neither can conjure a new member.
#
# The whole module is self-contained and deletable — one file to remove

View file

@ -78,9 +78,9 @@ in
'')
];
# ⚠️ `services.hyperhive.swarm.ui` no longer exists as a namespace. `enable`
# moved in an earlier slice and `package` in this one, and those were its
# only two options — so both paths survive purely as renames in ./deploy.nix.
# ⚠️ `services.hyperhive.swarm.ui` is not a namespace. Its two options,
# `enable` and `package`, live under `deploy.swarm-ui`, and the `swarm.ui`
# paths survive purely as renames in ./deploy.nix.
options.services.hyperhive.deploy.swarm-ui.package = lib.mkOption {
type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-ui";

View file

@ -262,6 +262,5 @@ in
# ⚠️ Like the metrics store's, that endpoint is unauthenticated — which is
# why `listenAddress` above is loopback and why nothing reaches it except
# through the gateway, where the ingest route is authenticated. (This module
# does declare a vhost; the line that used to say otherwise was already
# wrong before the collector started pushing through it.)
# declares a vhost.)
}

View file

@ -33,8 +33,8 @@ let
# explain their own admission rules; the second says why they are not one
# list. They are applied HERE rather than in the module that happens to
# consume them, because a hive name composes identifiers whatever else is
# enabled — ./swarm-otel.nix used to hold these guards and gated them on its
# own `enable`, so a swarm without the collector had no check at all.
# enabled — gating them on another module's `enable` would leave a swarm
# without that module with no check at all.
reservedNames = import ../reserved-names.nix;
reservedFragments = import ../reserved-hive-fragments.nix;
@ -213,7 +213,7 @@ in
# ⚠️ `lib.mkRemovedOptionModule` cannot do this job here, and
# neither of its two halves survives the move into a submodule:
# its `apply = throw` fires only when the value is READ, and
# nothing reads this any more (that being the point of removing
# nothing reads this (that being the point of removing
# it); its `config.assertions` half would land on a submodule
# that declares no `assertions` option at all. It is a
# top-level tool. The assertion below is the working shape, and
@ -316,14 +316,14 @@ in
assertions = [
{
# An EMPTY `hives` fires this too, deliberately: since
# `swarm.domain` became required, every hive is in a swarm — a
# `swarm.domain` is required, every hive is in a swarm — a
# swarm of one is still a swarm — so a directory with no entry
# for this host is missing one either way. It also has to fire
# here, because this host's own domain is now read out of the
# directory: without the entry `services.hyperhive.domain` is
# null and the generic required-domain assertion in
# ./hive-network.nix would fire instead, naming an option the
# operator should no longer be setting.
# operator should not be setting.
#
# Guarded on `hiveName != null` so the required-hiveName
# assertion in ./hyperhive.nix is what fires for that case —
@ -349,11 +349,10 @@ in
{
# An error rather than a warning, deliberately. Re-declaring the
# removed option above is what stops "option does not exist"; on
# its own it would also turn a config that used to FAIL into one
# that quietly evaluates with the setting ignored, which is a
# worse answer than the unhelpful error it replaces. The operator
# asked for a removal error, so this stays fatal until the field
# is gone from their config.
# its own it would turn a config that sets the removed field into
# one that quietly evaluates with the setting ignored, which is a
# worse answer than an error. So this stays fatal until the field
# is gone from the config.
#
# Wording follows `lib.mkRemovedOptionModule`'s, so it reads like
# every other removal the module system reports.
@ -536,8 +535,8 @@ in
# swarm's token endpoint. The other two coordinates are this machine's —
# where the queue is *as seen from here*, and where its secret sits on this
# disk — so they hang off the daemon that uses them. `hive-controller` is
# hive-c0re's new name, which is why the namespace is the daemon's rather
# than a `deploy.statusPublish` of its own.
# the hive-c0re daemon's namespace, hence these live there rather than
# under a `deploy.statusPublish` of their own.
#
# ⚠️ The all-or-nothing assertion above now spans both namespaces. That is
# one service's own options either side of the split, not a service reaching

View file

@ -59,7 +59,7 @@ let
{
# A homeserver URL is the whole input: from it the module derives the
# hive-internal `main` account, and from a non-empty account set the three
# things that used to hang off `matrix.enable`.
# matrix pieces: daemon unit, path watcher and injected MCP entry.
name = "an agent with a homeserver gets a main account and the matrix units";
ok =
let

View file

@ -101,7 +101,7 @@ let
ok = refusedWithoutIdentity controllerNoStore && !(refusedWithoutIdentity baoControllerHere);
}
{
# No hive mints an agent's matrix account any more, so a controller that
# No hive mints an agent's matrix account, so a controller that
# did not know the homeserver would create every agent without one. The
# default is the swarm's own `chat.` vhost, with no operator setting.
name = "the controller is told the swarm's homeserver by default";

View file

@ -26,10 +26,10 @@ let
deploy.bao.enable = true;
deploy.bao.extraListenAddresses = [ "10.0.0.1" ];
# Pinned, not incidental: the case counting these listeners is about the
# declared API addresses. ⚠️ Since the store got its own collector, this no longer keeps the METRICS
# listener out of the count — that one is a function of the store, not of
# a collector's placement — so the case below subtracts it by name rather
# than by leaving the swarm tier off.
# declared API addresses. ⚠️ Leaving the swarm tier off does not keep the
# METRICS listener out of the count — that one is a function of the
# store, not of a collector's placement — so the case below subtracts it
# by name.
deploy.swarm-otel.enable = false;
};
@ -156,11 +156,10 @@ let
&& (j.params.format or [ ]) == [ "prometheus" ];
}
{
# 🎯 This move's deliverable, and it needs BOTH halves or it is not a move:
# the job is in the store's own container, and it is no longer declared
# to the swarm collector. Only the second half can catch a "move" that
# left the original behind — two scrapers of one endpoint, double-counted
# and neither obviously wrong.
# 🎯 Needs BOTH halves: the job is in the store's own container, and it
# is not declared to the swarm collector. Only the second half can catch
# a scrape declared in both places — two scrapers of one endpoint,
# double-counted and neither obviously wrong.
name = "the store's metrics are scraped by its own collector, not the swarm's";
ok = baoScrapeJob baoWithCollector "bao" != null && scrapeJob baoWithCollector "bao" == null;
}
@ -206,20 +205,13 @@ let
j != null && !(j ? metrics_path) && !(j ? params);
}
{
# 🩸 This case USED to be the absence arm — "a store with no collector
# beside it serves no metrics" — and this inverts it deliberately
# rather than deleting it, because the condition it asserted is the bug.
#
# The old reasoning was sound for the old reader: the endpoint is
# unauthenticated, so it should not exist where nothing reads it, and
# the only reader was a swarm collector that had to be on this host.
# What that rendered on every OTHER host was a store whose metrics went
# nowhere, indistinguishably from a store nobody had asked to scrape.
#
# The reader now ships inside the container, so "no collector beside it"
# describes no host that exists. The endpoint is still not exposed to
# anything new: it is loopback in a netns whose only other occupants are
# the store and its collector.
# 🩸 The store's metrics endpoint does not depend on where the swarm
# collector runs. The endpoint is unauthenticated, so it should not exist
# where nothing reads it; the reader ships inside the container, so every
# store has one. Gating the endpoint on a collector beside the store
# would render a store whose metrics go nowhere, indistinguishable from
# a store nobody had asked to scrape. The endpoint is loopback in a
# netns whose only other occupants are the store and its collector.
name = "a store whose swarm collector is elsewhere still serves and scrapes its metrics";
ok =
let
@ -272,12 +264,11 @@ let
(j.merge or false) == true && j.directory == "/var/log/journal";
}
{
# The empty-directory trap this receiver used to fall into: `host` mode
# bind-mounts a host directory journald never actually writes into, so
# `merge`/`directory` above would keep validating and starting while
# the receiver reads nothing — silently, same as every other failure
# mode here. Guards against re-adding the exact flag this was fixed by
# dropping.
# The empty-directory trap: `host` mode bind-mounts a host directory
# journald never actually writes into, so `merge`/`directory` above
# would keep validating and starting while the receiver reads nothing —
# silently, same as every other failure mode here. Guards against the
# flag that links it.
name = "the store's container does not link its journal onto the host";
ok = !(lib.elem "--link-journal=host" (baoWithCollector.containers.swarm-bao.extraFlags or [ ]));
}
@ -291,7 +282,7 @@ let
{
# Both ends of the hop, because a mismatch between them is silent in both
# directions: the exporter retries into a 404 and the receiver never
# hears from it. Not "on one host" any more — the far end is the gateway
# hears from it. Not "on one host" — the far end is the gateway
# vhost the swarm collector serves its name on, which is the whole point
# of addressing it by name rather than by a bridge address.
name = "the store's forwarder exports to the swarm collector's own route";

View file

@ -4,9 +4,8 @@
#
# Where the forge runs. A swarm has one, on the host with
# `deploy.forgejo.enable`; every other hive is a client of it, and the parts
# of a split deployment that used to lean on every host running a forge
# (the OIDC client, the controller's token, the CI runner) still have to
# hold.
# of a split deployment that must not assume every host runs a forge
# (the OIDC client, the controller's token, the CI runner) have to hold.
{
pkgs,
lib,

View file

@ -70,13 +70,11 @@ let
deploy.bao.grafanaOidcClientKeyFile = "/etc/pki/bao-grafana-oidc-key.pem";
};
# A Grafana host holding no store identity. This used to be the shape the
# module went QUIET on — no OIDC block, a warning, and a container whose
# login form is off regardless, so no way in and nothing failed. It is kept
# rather than deleted because the shape is still reachable by an operator;
# what changed is the deliverable, from a warning nothing reads back to a
# refusal naming the two options to set. Only the identity is missing, so an
# arm below can name which refusal fired.
# A Grafana host holding no store identity. The module refuses it with an
# assertion naming the two options to set, rather than going QUIET: no OIDC
# block can render and the container's login form is off regardless, so
# there would be no way in and nothing would fail. Only the identity is
# missing, so an arm below can name which refusal fired.
grafanaNoIdentity = hive {
deploy.grafana.enable = true;
deploy.grafana.plugins = [ ];
@ -104,9 +102,9 @@ let
) m.assertions;
cases = [
{
# This fixture enables grafana and NOT authelia, which is the shape the
# login form used to stay enabled in: the toggle read "both services are
# on this host" rather than "grafana requires SSO". Grafana ships an
# This fixture enables grafana and NOT authelia, so the login-form toggle
# has to follow "grafana requires SSO" rather than "both services are on
# this host". Grafana ships an
# `admin`/`admin` account and its vhost is on the public gateway, so a
# password box there is a way in whatever the topology.
name = "grafana disables its local login form even where authelia is not on this host";

View file

@ -23,8 +23,8 @@ let
# The hive-name guards, with the collector explicitly OFF. That is the whole
# property: the guards live where `swarm.hives` is declared, so they run in a
# deployment that has a secret store and no collector — which used to skip
# them entirely, because they were assertions inside swarm-otel's own `mkIf`.
# deployment that has a secret store and no collector, rather than being
# assertions inside swarm-otel's own `mkIf`.
#
# ⚠️ `controllerCommonName` is overridden to a name containing NO reserved
# fragment. Its default (`swarm-controller`) contains `swarm` and is caught

View file

@ -44,9 +44,9 @@ let
# refuses; see ./host-modules/deploy.nix. `package` and `bridgePackage` are
# ordinary options, so they do carry one. The two arms
# below have different jobs. `usersFile` tests the rename; the nats one tests
# that a reader repointed to the new namespace still renders the derived
# path, which is the failure this slice could actually have shipped — seven
# of those reads went through an alias a path-shaped grep cannot see.
# that a reader pointed at the `deploy` namespace still renders the derived
# path — seven of those reads go through an alias a path-shaped grep
# cannot see.
autheliaOldPath = hive {
deploy.authelia.enable = true;
deploy.nats.enable = true;
@ -84,8 +84,8 @@ let
# A hive running NOTHING of the swarm's own services — no IdP here, no
# `swarm.authelia.url` set by hand. The whole point of the fixture is what it
# does *not* say: it is the shape whose IdP address used to be null, and
# before that a co-location-derived guess.
# does *not* say: no IdP address is given and none is derived from
# co-location.
autheliaNotColocated = hive { };
# The same swarm's IdP host, for the arm that the two agree.
autheliaColocated = hive { deploy.authelia.enable = true; };

View file

@ -36,9 +36,7 @@ let
# deployment this unit exists for: authelia is the one host the store is
# guaranteed not to share once either has a machine of its own.
#
# ⚠️ `enable` is deliberately NOT set here. It used to be, with a comment
# saying the default asked whether both ran on this host — which documented
# the co-location bug instead of catching it. Leaving it unset is what makes
# ⚠️ `enable` is deliberately NOT set here: leaving it unset is what makes
# this fixture exercise the default rather than mask it.
secretPublisherRemote = hive {
deploy.authelia.enable = true;
@ -125,9 +123,9 @@ let
&& !(lib.elem "swarm-grafana" (clients grafanaRemoteAuthelia));
}
{
# The collector's half of the same defect and the same fix: this used to
# be gated on `deploy.swarm-otel.enable`, so a split deployment
# registered the client nowhere and authelia minted nothing to publish.
# The collector's half of the same property: the client is not gated on
# `deploy.swarm-otel.enable`, which would let a split deployment register
# it nowhere, so authelia would mint nothing to publish.
name = "the swarm's collector client is registered wherever authelia runs";
ok =
let

View file

@ -112,10 +112,10 @@ let
&& !(lib.hasInfix "secret/swarm/hives/" s);
}
{
# The defect itself. These exporters used to be gated on the stores'
# PER-HOST enables, so a collector that did not share a host with them
# rendered none at all and dropped everything it received, from every
# hive — silently, because an absent exporter is not an error.
# These exporters must not be gated on the stores' PER-HOST enables: a
# collector that did not share a host with them would render none at all
# and drop everything it received, from every hive — silently, because
# an absent exporter is not an error.
name = "a collector that hosts neither store still exports to both";
ok =
let

View file

@ -61,10 +61,10 @@ let
# derivation means a prompt edit doesn't bust the cargo cache.
#
# …and the dep cache underneath is the one WITHOUT test targets. This
# derivation has never run a test — `doCheck = false` below says so —
# but it used to sit on a cache that compiled every dev-dependency and
# test harness anyway, work whose only consumers are the checks. A
# deploy paid for it on every toolchain or dependency change.
# derivation never runs a test — `doCheck = false` below says so — so it
# sits on a cache that skips every dev-dependency and test harness, work
# whose only consumers are the checks. Compiling them here would make a
# deploy pay for it on every toolchain or dependency change.
workspaceBuild = craneLib.buildPackage {
src = cleanSrc;
cargoArtifacts = cargoArtifactsBinOnly;

View file

@ -38,9 +38,9 @@ buildNpmPackage {
# Read from `../../frontend/npm-deps-hash`, the single source of
# truth `./swarm-ui.nix` and `../checks.nix`'s `swarm-ui-typecheck`
# also read — all three build from the one `frontend/package-lock.json`,
# so one file keeps them from drifting independently (this used to be
# a hardcoded copy per derivation, and only some of the three got
# updated the one time the lockfile changed). Regenerate with
# so one file keeps them from drifting independently (a hardcoded copy
# per derivation would let some of the three miss a lockfile change).
# Regenerate with
# `prefetch-npm-deps frontend/package-lock.json` and overwrite the
# file whenever the lockfile changes.
npmDepsHash = builtins.readFile ../../frontend/npm-deps-hash;

View file

@ -5,10 +5,10 @@
# Pure vendored Swagger UI 5.17.14 static dist — no hyperhive theming
# (see ./swagger-ui-theme.nix for that layer). Deliberately NOT sourced
# via the Rust `utoipa-swagger-ui-vendored` crate / Cargo.lock (per the
# operator's ask: a nix package that just contains the plain static
# dist, not fetched via cargo) — hive-c0re no longer needs to know this
# exists at all once the gateway hosts it directly.
# via the Rust `utoipa-swagger-ui-vendored` crate / Cargo.lock: this is a
# nix package that just contains the plain static dist, not fetched via
# cargo — hive-c0re does not need to know this exists, because the
# gateway hosts it directly.
#
# Sourced straight from the `swagger-ui-dist` npm package, which ships
# exactly the built `dist/` files (no source, no build step needed) at

View file

@ -21,7 +21,7 @@
# `{{config}}` placeholder its OWN build.rs
# injects (`replace_default_url_with_config`
# + `format_config`); hive-c0re doesn't serve
# this file at all anymore, so that rewrite
# this file at all, so that rewrite
# is baked in statically here instead —
# hand-verified byte-for-byte against what
# `format_config` would produce, by applying

View file

@ -56,8 +56,8 @@
# The swarm collector names its components `<kind>/<owner>` and uses the hive
# name as the owner, so a hive called `swarm` would silently replace the
# swarm tier's own pipelines and lose its own — it would keep accepting
# pushes into a pipeline that routes nowhere. Previously enforced only
# against hive names, in `swarm-otel.nix`'s own `reservedOwners`.
# pushes into a pipeline that routes nowhere. `swarm-otel.nix` asserts
# this name stays in the list, since its own `reservedOwners` reads it.
"swarm"
# ---- usernames the forge refuses -------------------------------------