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

View file

@ -10,8 +10,8 @@
let let
userName = config.services.hyperhive.agent.user.name; userName = config.services.hyperhive.agent.user.name;
# The token the agent fetched from the swarm secret store # The token the agent fetched from the swarm secret store
# (./forge-token.nix), then the state-dir file the hive used to write, # (./forge-token.nix), then the state-dir file the hive writes, which is
# which is still the only copy for an agent without a store identity. # the only copy for an agent without a store identity.
# Both are PATHS; each reader below takes the first that holds a token. # Both are PATHS; each reader below takes the first that holds a token.
fetchedTokenFile = config.services.hyperhive.agent.forge.tokenFile; fetchedTokenFile = config.services.hyperhive.agent.forge.tokenFile;
stateTokenFile = "/agents/${userName}/state/forge-token"; 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 # 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 # 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 # 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 # No cycle: `matrixAccounts`'s own definition reads `matrix.url`, never this
# binding, so the `mkIf`s below may read the merged option value freely. # 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 then talks to the wrong machine. An absent integration, never a
misdirected one. misdirected one.
`null` is also this agent's **matrix off switch**, and the `null` is also this agent's **matrix off switch** — there is no
replacement for the `services.hyperhive.agent.matrix.enable` separate `services.hyperhive.agent.matrix.enable` boolean: the
boolean that used to exist: the hive-internal `main` account in hive-internal `main` account in
`services.hyperhive.agent.matrixAccounts` is declared from this `services.hyperhive.agent.matrixAccounts` is declared from this
URL, so `null` leaves that set empty and the whole integration — URL, so `null` leaves that set empty and the whole integration —
daemon unit, path watcher, injected MCP entry — is not generated daemon unit, path watcher, injected MCP entry — is not generated
@ -214,12 +214,11 @@ in
config = { config = {
assertions = [ assertions = [
# The "extras require `matrix.enable`" assertion that used to head this # There is no "extras require `matrix.enable`" assertion: a non-empty
# list is gone with the option: a non-empty account set is now what # account set is what enables matrix, so that condition is the
# enables matrix, so the condition it checked has become the definition # definition of the thing it would check. An operator declaring only an
# of the thing it was checking. An operator declaring only an external # external account, with no hive homeserver, gets a rendered config —
# account, with no hive homeserver, is a config this module now renders # matrix on, no `main`.
# rather than rejects — matrix on, no `main`.
# #
# `main` is not a forbidden key --- this module declares it # `main` is not a forbidden key --- this module declares it
# itself (see the `matrixAccounts.main` definition below), so the # 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>`, # Per-agent options live at `services.hyperhive.agent.<name>`, beside the host
# while the host tier has always been under `services.hyperhive.*`. The agent # tier's `services.hyperhive.*`. These entries keep an `agent.nix` that still
# tier moved to `services.hyperhive.agent.<name>`; these entries keep an # uses the top-level `hyperhive.<name>` spelling evaluating, with a
# existing `agent.nix` evaluating (with a deprecation warning) against the old # deprecation warning.
# spelling.
# #
# ⚠️ One entry per LEAF, not per namespace. A single entry for a parent path # ⚠️ 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 # only covers its children when the parent is a submodule-typed option; every
@ -119,9 +118,9 @@
[ "hyperhive" "logs" "queryUrl" ] [ "hyperhive" "logs" "queryUrl" ]
[ "services" "hyperhive" "agent" "logs" "queryUrl" ] [ "services" "hyperhive" "agent" "logs" "queryUrl" ]
) )
# `matrix.enable` is deliberately absent from this list: the option it # `matrix.enable` is deliberately absent from this list: it has no
# renamed to no longer exists, and a rename pointing at an undeclared # replacement option, and a rename pointing at an undeclared option
# option breaks evaluation for everyone. Both of its spellings are # breaks evaluation for everyone. Both of its spellings are
# `mkRemovedOptionModule` in ./default.nix instead. # `mkRemovedOptionModule` in ./default.nix instead.
(lib.mkRenamedOptionModule (lib.mkRenamedOptionModule
[ "hyperhive" "matrix" "url" ] [ "hyperhive" "matrix" "url" ]

View file

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

View file

@ -86,8 +86,8 @@ in
clientKeyFile = lib.mkDefault "${pkiDir}/client-key.pem"; clientKeyFile = lib.mkDefault "${pkiDir}/client-key.pem";
serverCaFile = lib.mkDefault "${pkiDir}/ca.pem"; serverCaFile = lib.mkDefault "${pkiDir}/ca.pem";
# 🩸 Readers that used to present `client.pem` above, each now pointed at # 🩸 The readers below each present a leaf of their own rather than
# a leaf of its own. The leaf is what bao sees, so this pairing is the # `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 # whole of what turns "one principal with the union of their grants" into
# one principal per grant — see ./swarm-bao.nix's `perHiveReaders` block # one principal per grant — see ./swarm-bao.nix's `perHiveReaders` block
# for the grants themselves. # for the grants themselves.
@ -193,11 +193,11 @@ in
[ -s ${pkiDir}/matrix-ctl.pem ] || ${signLeaf} ${pkiDir} matrix-ctl \ [ -s ${pkiDir}/matrix-ctl.pem ] || ${signLeaf} ${pkiDir} matrix-ctl \
${lib.escapeShellArg deployCfg.bao.matrixCtlCommonName} "" clientAuth ${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 # 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 # cert-auth role on the CN, so units sharing one leaf would be one
# holding the union of their grants, and the union included every agent # principal holding the union of their grants, and the union would
# credential in the swarm. # include every agent credential in the swarm.
# #
# ⚠️ The first carries THIS hive's name in the subject, unlike the three # ⚠️ 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 # 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 # 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. # and on a hive three networks away for exactly the same reason.
# #
# 🩸 This principal's OWN leaf, not `clientCertFile` — the hive's, which four # 🩸 This principal's OWN leaf, not `clientCertFile` — the hive's. Bao
# units used to share. Bao matches a cert-auth role on the CN, so one leaf for # matches a cert-auth role on the CN, so one leaf shared by four readers
# four readers was ONE principal holding the union of four grants: read on # would be ONE principal holding the union of four grants: read on
# `swarm/agents/*` AND `swarm/hives/<hive>/*` AND `swarm/services/*`, when # `swarm/agents/*` AND `swarm/hives/<hive>/*` AND `swarm/services/*`, when
# this unit reads one queue credential and nothing else. Its own leaf carries # this unit reads one queue credential and nothing else. Its own leaf carries
# `<queueAgentCommonNamePrefix>-<hive>` and its role grants the single path # `<queueAgentCommonNamePrefix>-<hive>` and its role grants the single path
@ -53,11 +53,11 @@ let
baoDeploy.queueAgentClientCertFile != null && baoDeploy.queueAgentClientKeyFile != null; baoDeploy.queueAgentClientCertFile != null && baoDeploy.queueAgentClientKeyFile != null;
# Does this host read the store at all — the hive's own leaf, which is the # 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 # one thing a remote-store deployment has to place by hand. Only used to
# used to decide whether a missing per-principal leaf is a mistake or a # decide whether a missing per-principal leaf is a mistake or a deployment
# deployment that has no store: a host holding neither is the supported # that has no store: a host holding neither is the supported no-store
# no-store shape, and one holding this pair but not the pair above stopped # shape, and one holding this pair but not the pair above reads the store
# short of naming this reader's own. # without a reader identity of its own.
hiveReaderIdentity = baoDeploy.clientCertFile != null && baoDeploy.clientKeyFile != null; hiveReaderIdentity = baoDeploy.clientCertFile != null && baoDeploy.clientKeyFile != null;
credentialDir = toString deployCfg.hive-controller.queue.agentCredentialDir; credentialDir = toString deployCfg.hive-controller.queue.agentCredentialDir;

View file

@ -115,17 +115,14 @@ in
HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES = otel.extraResourceAttributes; HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES = otel.extraResourceAttributes;
} }
# HYPERHIVE_OTEL_HEADERS_CREDENTIAL is deliberately NOT emitted, and # HYPERHIVE_OTEL_HEADERS_CREDENTIAL is deliberately NOT emitted, and
# its absence is the security half of this design: it was the variable # its absence is the security half of this design: that variable would
# that put the upstream token into an agent's own settings.json. The # put the upstream token into an agent's own settings.json. Nothing
# delivery path it drove — an nspawn credential forwarded by # delivers the token into the container — no nspawn credential, no
# host_config.rs, then written to an agent-readable file by # agent-readable header file.
# claude-settings.nix's `hive-otel-header` oneshot — no longer exists
# anywhere; it was removed along with this variable's last consumer.
# #
# Kept as a comment rather than deleted because the useful part is the # The rule: the collector holding the credential achieves nothing while
# RULE, not the history: the collector holding the credential achieves # anything else hands out a copy, so there is exactly one holder and it
# nothing while anything else hands out a copy, so there is exactly one # is on the host.
# holder and it is on the host.
// lib.optionalAttrs (otel.metricIntervalMs != null) { // lib.optionalAttrs (otel.metricIntervalMs != null) {
HYPERHIVE_OTEL_METRIC_INTERVAL_MS = toString otel.metricIntervalMs; 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` # `caTrust.containerOrdering` orders this unit after `hive-tls-ca.service`
# in self-signed mode (see the hive-ca-trust helper). Runner registration # 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 — # is not on the boot-critical path — hive-c0re owns it out of band — so
# so the container boots immediately and needs no start-timeout bump. The # the container boots immediately and needs no start-timeout bump.
# 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.
systemd.services."container@hive-ci" = caTrust.containerOrdering; systemd.services."container@hive-ci" = caTrust.containerOrdering;
containers.hive-ci = { containers.hive-ci = {
@ -415,7 +412,7 @@ in
# runner stays down (Restart=on-failure retries) until c0re # runner stays down (Restart=on-failure retries) until c0re
# registers it, rather than blocking the container's start. # 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 # (bound socket above), which is up before the container — nix's
# own connect retries cover any brief window. `mkBefore` runs this # own connect retries cover any brief window. `mkBefore` runs this
# ahead of any upstream pre-steps. # ahead of any upstream pre-steps.

View file

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

View file

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

View file

@ -55,10 +55,9 @@ let
# `cfg.useSelfSigned` (options.nix) is the derived single source of truth. # `cfg.useSelfSigned` (options.nix) is the derived single source of truth.
useSelfSigned = cfg.useSelfSigned; useSelfSigned = cfg.useSelfSigned;
# nginx's own state dir. Kept at the historical `/var/lib/hive-gateway` # nginx's own state dir, fixed at `/var/lib/hive-gateway` independent of
# path rather than renamed with the move: it holds the imported leaf # this module's own path: it holds the imported leaf across reboots, and
# across reboots, and renaming it would strand every existing hive's # a different path would strand every existing hive's certs.
# certs for no gain.
tlsDir = "/var/lib/hive-gateway/tls"; tlsDir = "/var/lib/hive-gateway/tls";
# TLS cert + key paths. # TLS cert + key paths.
# - self-signed (default): the hive leaf the store issues (the # - 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 # before c0re has run, and pin owner + mode rather than leaving it
# to whoever creates the path first. # to whoever creates the path first.
# #
# ⚠️ There is deliberately NO rule for /var/lib/hyperhive here. One # ⚠️ There is deliberately NO rule for /var/lib/hyperhive here.
# used to declare it `0755 root root` and could never win:
# `hive-c0re.service` sets `StateDirectory = "hyperhive"` with # `hive-c0re.service` sets `StateDirectory = "hyperhive"` with
# `StateDirectoryMode = "0750"`, which systemd re-applies on every # `StateDirectoryMode = "0750"`, which systemd re-applies on every
# start. Two mechanisms owning one path, and the loser still read as # start, so a tmpfiles rule for the same path could never win: two
# a guarantee — it is why the resulting outage was first misdiagnosed # mechanisms owning one path, the loser still reading as a guarantee.
# as someone having changed the mode. c0re's own unit owns that dir; # c0re's own unit owns that dir; this module has no opinion about it.
# this module no longer has an opinion about it.
systemd.tmpfiles.rules = [ systemd.tmpfiles.rules = [
# The gateway's own config dir — NOT under /var/lib/hyperhive. c0re # The gateway's own config dir — NOT under /var/lib/hyperhive. c0re
# writes here, nginx reads here, and neither needs any access to the # 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 # ⚠️ NO `SupplementaryGroups = [ "hive-core" ]` on nginx, and its
# absence is load-bearing rather than an omission. # absence is load-bearing rather than an omission.
# #
# It used to be here, to let nginx traverse `/var/lib/hyperhive` and # `hive-c0re.service` declares `StateDirectory = "hyperhive"` with
# reach the config fragments that lived inside: `hive-c0re.service` # `StateDirectoryMode = "0750"` owned by `hive-core`, and systemd
# declares `StateDirectory = "hyperhive"` with `StateDirectoryMode = # re-applies that on every c0re start. The group would let nginx
# "0750"` owned by `hive-core`, and systemd re-applies that on every # traverse that dir, but it also gives nginx read access to everything
# c0re start — beating this module's own tmpfiles rule for the same # ELSE group-readable under it, including the broker sqlite — i.e.
# path. Without the group, nginx failed its config test and never # every message between every agent, reachable by the process whose
# started (`nginx: [emerg] open() ".../agents.conf" failed (13: # entire job is parsing untrusted network input. The config fragments
# Permission denied)`), taking the gateway and every hive domain # nginx reads live in the gateway's own dir instead, so nginx needs no
# behind it down. # access to c0re's tree. Adding this line would reopen that exposure.
#
# 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.
networking.firewall = lib.mkIf cfg.openFirewall { networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ allowedTCPPorts = [
@ -424,12 +413,11 @@ in
# every name a service module contributed. See `docs/networking/gateway.md` # every name a service module contributed. See `docs/networking/gateway.md`
# ("Local dev"). # ("Local dev").
# #
# This used to restate the per-service list a THIRD time (after the # This does not restate the per-service list (the vhosts and the
# vhosts and the dnsmasq records), with its own copy of each # dnsmasq records already carry it). It is the same question —
# service's guard. It is the same question — "which names does this # "which names does this host answer for" — so it reads the same
# host answer for" — so it reads the same answer; a service added # answer; a service added later lands here with no edit, and cannot
# later lands here with no edit, and cannot land here with a # land here with a different condition than it used for DNS.
# different condition than it used for DNS.
networking.hosts = lib.mkIf cfg.localHostsEntry { networking.hosts = lib.mkIf cfg.localHostsEntry {
"127.0.0.1" = lib.unique ([ hyperhiveDomain ] ++ cfg.localNames); "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 nginx vhost. Leave off when running behind another reverse
proxy (e.g. caddy / traefik on the host) that handles TLS proxy (e.g. caddy / traefik on the host) that handles TLS
termination + forwards to `port`. 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` — # Swagger UI: nginx hosts the FULL themed dist (`swaggerUiTheme` —
# vendored Swagger UI + our overlay, see nix/packages/swagger-ui- # vendored Swagger UI + our overlay, see nix/packages/swagger-ui-
# dist.nix + swagger-ui-theme.nix) straight from the store, with NO # dist.nix + swagger-ui-theme.nix) straight from the store, with NO
# fallback to hive-c0re at all — per the operator's shape: "core # fallback to hive-c0re at all: core hosts only the JSON. Only
# should not need the swagger ui at all if it is hosted in gateway" # `/api/openapi.json` (the live-generated spec `index.html` fetches;
# / "core only hosts the json". Only `/api/openapi.json` # not under this prefix) proxies to c0re via "/api/" below — that's
# (the live-generated spec `index.html` fetches; not under this # the one thing that has to stay dynamic.
# prefix) still 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 location, not exact-match: wins over "/api/" on plain
# prefix length (no ordering/`=` needed), and now needs to cover # prefix length (no ordering/`=` needed), and covers every file in
# every file in the tree (bundle.js, maps, favicons, …), not just # the tree (bundle.js, maps, favicons, …), not just our 2 override
# our 2 override files — hive-c0re no longer serves any of this as # files — hive-c0re serves none of this as a fallback.
# a fallback once its own `utoipa-swagger-ui` mount is removed. # `= /api/docs` (no trailing slash) redirects to `/api/docs/`: nginx
# `= /api/docs` (no trailing slash) issues the same redirect # owns that redirect because hive-c0re has no route for it, and the
# `utoipa-swagger-ui`'s router used to: that mount is going away # H0M3 hub's own `/api/docs` link (no trailing slash) would 404
# too, so nginx has to own it now, or the H0M3 hub's own `/api/docs` # without it.
# link (no trailing slash) would 404 once hive-c0re drops the route.
swaggerUiLocations = { swaggerUiLocations = {
"= /api/docs" = { "= /api/docs" = {
extraConfig = '' extraConfig = ''

View file

@ -153,12 +153,11 @@ let
# to host it — otherwise moving the container between hives would look # to host it — otherwise moving the container between hives would look
# like a different homeserver. # like a different homeserver.
# #
# ⚠️ Changing this default is a BREAKING change in a way that moving # ⚠️ Changing this default is breaking: `serverName` is baked
# `gatewayHost` was not: `serverName` is baked irrevocably into every # irrevocably into every user and room id, so a deployment that
# user and room id, so a deployment that rebuilds onto a new one is a # rebuilds onto a new one is a *different homeserver*. Existing hives
# *different homeserver*, not a renamed one. Existing hives pin the old # pin their value explicitly (see the option's description); the
# value explicitly (see the option's description); the default is what # default is what a fresh swarm gets.
# a fresh swarm gets.
# #
# Total on a null swarm domain, deliberately: the required-domain # Total on a null swarm domain, deliberately: the required-domain
# assertion in hive-network.nix is what should fire, not a coercion # 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 # settings: docs/integrations/matrix.md. Vhost map + discovery flow + tuning
# knobs: docs/networking/gateway.md. # knobs: docs/networking/gateway.md.
# Matrix moved under `swarm` when the swarm-global services were # Matrix lives under `swarm`, beside the other swarm-global services. One
# consolidated. One rename for the namespace: the subtree comes with it, # rename for the top-level `services.hyperhive.matrix` namespace: the
# so existing hives keep evaluating and get one warning naming both paths. # subtree comes with it, so existing hives keep evaluating and get one
# warning naming both paths.
imports = [ imports = [
(lib.mkRenamedOptionModule (lib.mkRenamedOptionModule
[ "services" "hyperhive" "matrix" ] [ "services" "hyperhive" "matrix" ]
@ -311,11 +311,6 @@ in
the homeserver to other hives or when an external matrix the homeserver to other hives or when an external matrix
client needs to reach the client-server API directly. 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 Note: federation (the matrix-spec well-known port 8448) is
intentionally not opened here. tuwunel serves the federation intentionally not opened here. tuwunel serves the federation
API on the same `httpPort` as the client-server API by 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 # 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 # than a stale one — its own vhosts are covered by the hive leaf
# above. Anywhere else it is expected: the leaf comes from the # 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 # secret store's `pki` mount, reachable from every host rather than
# one host in the swarm, so "this host cannot issue it" is no longer # from a sub-CA on one host in the swarm, so "this host cannot issue
# one of the answers. # it" is not one of the answers.
want_svc=${if localServiceDomains == [ ] then "0" else "1"} want_svc=${if localServiceDomains == [ ] then "0" else "1"}
# Expiry is not the only way a leaf goes wrong. One signed when the # Expiry is not the only way a leaf goes wrong. One signed when the
@ -346,17 +346,14 @@ in
}; };
config = lib.mkIf active { config = lib.mkIf active {
# 🚫 The "these service names are outside this hive's domain and this # 🚫 There is deliberately NO warning that these service names are
# host cannot sign for them" warning is GONE, and its absence is the # outside this hive's domain and this host cannot sign for them. The
# deliverable rather than a tidy-up. It fired on # services leaf comes from the secret store's `pki` mount, which every
# `!swarm.ca.autoConfigure` — i.e. "this host does not hold the swarm # hive reaches over the network with its own identity, so holding the
# root key" — which was the whole reason a hive could end up serving # swarm root key does not decide whether a hive can serve a
# its own leaf on a swarm-service name. The services leaf now comes # swarm-service name. Such a warning would fire on every rebuild of
# from the secret store's `pki` mount, which every hive reaches over # every hive that is not the CA host, about a fallback that does not
# the network with its own identity, so holding the root key stopped # happen.
# 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.
# The state dir and the trust bundle. Never waits on the store: the # The state dir and the trust bundle. Never waits on the store: the
# store's container is ordered after this unit, which writes 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 # Where the swarm lives. Declared beside the hive's own identity
# because it is what that identity is derived FROM — every hive in a # 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 # swarm is a sub-domain of it. No alias: this option has no earlier
# is genuinely new: nothing moved here, so there is no alias. # spelling.
options.services.hyperhive.swarm.domain = lib.mkOption { options.services.hyperhive.swarm.domain = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
default = null; default = null;
@ -145,7 +145,7 @@ in
(`services.hyperhive.deploy.hive-controller.enable`, (`services.hyperhive.deploy.hive-controller.enable`,
`deploy.bao.enable`, `deploy.matrix.enable`). Distinct from `deploy.bao.enable`, `deploy.matrix.enable`). Distinct from
`services.hyperhive.domain` (the machine-addressable DNS name) `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 `<hiveName>.<swarm.domain>`, so this is the label the hive is
*addressed* by as well as the one it is called. Exposed to *addressed* by as well as the one it is called. Exposed to
agents as `HYPERHIVE_HIVE_NAME`; surfaced in the dashboard agents as `HYPERHIVE_HIVE_NAME`; surfaced in the dashboard

View file

@ -60,9 +60,8 @@ in
# rather than the file. # rather than the file.
# #
# ⛔ **Do not hand this path to a consumer that reads only ONE # ⛔ **Do not hand this path to a consumer that reads only ONE
# certificate from it.** This comment used to offer an OIDC verifier's # certificate from it** — an OIDC verifier's `issuer_ca_path` is
# `issuer_ca_path` as the motivating example; that is exactly what took # exactly such a reader, and takes the swarm collector down. The bundle is
# the swarm collector down for forty minutes once. The bundle is
# `system CAs ++ hive anchors`, so the anchor is ~123rd, and a # `system CAs ++ hive anchors`, so the anchor is ~123rd, and a
# first-certificate-only reader gets whichever public CA sorts first and # first-certificate-only reader gets whichever public CA sorts first and
# can verify nothing of ours — with a full, valid, 125-certificate file # 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 ⚠️ The collector is therefore in the path of all telemetry. It runs
on the same host as the agents and restarts on failure, and on the same host as the agents and restarts on failure, and
telemetry is not the control plane, so degraded telemetry is not 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 of anything host-side
''; '';
@ -239,7 +239,7 @@ in
onward, which is one hop for the whole hive rather than one per onward, which is one hop for the whole hive rather than one per
target. 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. is always in here, so the scrape receiver is emitted everywhere.
The `!= { }` guards around it stay meaningful — the set can still The `!= { }` guards around it stay meaningful — the set can still
be forced empty, and a `prometheus` receiver with nothing to 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. tracked as a follow-up rather than papered over.
On by default: a swarm's hives have identities, and that is a 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 fact about the swarm rather than about any one host. It does not
default to whether the queue ran on *this* machine, which made depend on whether the queue runs on *this* machine, so the answer
the answer differ between two hosts of one swarm — set it false is the same on every host of one swarm — set it false for a swarm
for a swarm whose hives authenticate to nothing. whose hives authenticate to nothing.
The clients are inert until something authenticates with them: The clients are inert until something authenticates with them:
each is a client id and a secret sitting on this host. What 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 # 🏷️ 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 # 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 # `<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 # 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. # match site, and the assertion below covers the case that ordering cannot.
# #
@ -905,11 +905,11 @@ in
}; };
environment = { environment = {
SWARM_AUTHELIA_BRIDGE_BIND = "127.0.0.1:${toString cfg.bridgePort}"; SWARM_AUTHELIA_BRIDGE_BIND = "127.0.0.1:${toString cfg.bridgePort}";
# ONE file, and this is it. The bridge used to carry a # ONE file, and this is it. The bridge keeps no second,
# second, private `…-users.json` it treated as canonical # private users store: a store of its own treated as canonical,
# while writing `users.yml` as a rendering of it — two # with `users.yml` written as a rendering of it, would be two
# canonical stores for one physical file, which is what # canonical stores for one physical file, and would make
# made `swarm agent create` refuse to start on a hive whose # `swarm agent create` refuse to start on a hive whose
# `users.yml` already held users. # `users.yml` already held users.
SWARM_AUTHELIA_BRIDGE_USERS_FILE = deployCfg.authelia.usersFile; SWARM_AUTHELIA_BRIDGE_USERS_FILE = deployCfg.authelia.usersFile;
# The CONFIGURED authelia, not whatever is on `PATH`: the # 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 of these reads exactly ONE path in the store, and does it holding
# each did it holding `deploy.bao.clientCertFile` — the hive's own leaf, # its own leaf rather than `deploy.bao.clientCertFile` — the hive's own leaf,
# whose policy (`swarm-secret-client`'s `policy::render`) granted read on # whose policy (`swarm-secret-client`'s `policy::render`) grants read on
# `swarm/agents/*`, `swarm/hives/<hive>/*` AND `swarm/services/*`. Principals # `swarm/agents/*`, `swarm/hives/<hive>/*` AND `swarm/services/*`.
# presenting one certificate are ONE principal to bao, so the union of what # Principals presenting one certificate are ONE principal to bao, so sharing
# they need was the only grant expressible: the unit that fetches Grafana's # the hive's leaf would leave only the union of what they need expressible:
# OIDC client secret could fetch every agent credential in the swarm, and the # the unit that fetches Grafana's OIDC client secret could fetch every agent
# unit that fetches this hive's queue credential could fetch Grafana's. Least # credential in the swarm, and the unit that fetches this hive's queue
# privilege was not misconfigured, it was unrepresentable. # credential could fetch Grafana's.
# #
# Each now gets its own leaf, its own cert-auth role and a policy holding the # 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 # one `secret/data/…` path that principal's script actually names — spelled
# the LEAF rather than to a prefix, for the reason `matrixCtlPolicyText` above # to the LEAF rather than to a prefix, for the reason `matrixCtlPolicyText`
# gives for doing the same. # above gives for doing the same.
# #
# ⚠️ The queue-credential reader is PER HIVE and the OIDC readers are not, # ⚠️ 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 # 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 # `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 # 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 # serve every hive — which would let one hive read another's queue
# credential. # credential, a reach no hive needs.
readStanza = path: '' readStanza = path: ''
path "${path}" { path "${path}" {
capabilities = ["read"] capabilities = ["read"]
@ -1024,10 +1024,9 @@ let
# `tls_require_and_verify_client_cert` on the API listener to scrape THAT # `tls_require_and_verify_client_cert` on the API listener to scrape THAT
# instead, would each trade that property away for nothing. # instead, would each trade that property away for nothing.
# #
# Unconditional, and that is the change this carries: the reader is now the # Unconditional: the reader is the collector in this very container (below),
# collector in this very container (below), which renders wherever the store # which renders wherever the store does. There is no host where this
# does. There is no longer a host where this endpoint has no reader, so there # endpoint has no reader, so it is not a function of anything.
# is nothing left for it to be a function of.
metricsListener = { metricsListener = {
metrics = { metrics = {
type = "tcp"; type = "tcp";
@ -1104,22 +1103,18 @@ let
''; '';
# ⚠️ Neither the listener above nor the forwarder below has a condition of # ⚠️ 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` # The listener's reader is the collector in this container, which sits
# — "does the swarm's collector happen to run on THIS host" — because the # wherever the store does. "Does the swarm's collector run on THIS host" is
# thing reading it was that collector, over loopback, which only works when # not a question it depends on: gating it on that would leave the metrics
# the two land together. mara's ruling (*"move the existing scraper # path silently dead on every host that does not also run swarm-otel.
# 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 forwarder never had a defensible one. "Does the swarm have a collector" # "Does the swarm have a collector" is not a question that can be false —
# is not a question that can be false (mara: "the swarm always has at least # the swarm always has at least one instance of all the swarm level
# one instance of all the swarm level services"), so both earlier gates — # services — so gating the forwarder on `otel.enable` or
# `otel.enable`, then `deploy.swarm-otel.enable` — asked about the wrong host # `deploy.swarm-otel.enable` would ask about the wrong host and render no
# and rendered no forwarder wherever the answer happened to be no. # forwarder wherever the answer happened to be no.
# #
# The only condition that belongs on either is the one already enclosing this # The only condition that belongs on either is the one already enclosing this
# block: the store is deployed on this host. # block: the store is deployed on this host.
@ -2133,14 +2128,13 @@ in
# granter step, on the host where that step runs. # granter step, on the host where that step runs.
environment.etc."hyperhive/bao-bootstrap-policy.hcl".source = ./bao-bootstrap-policy.hcl; environment.etc."hyperhive/bao-bootstrap-policy.hcl".source = ./bao-bootstrap-policy.hcl;
# 🚫 No `swarm.otel.scrapeTargets.bao` entry any more, and its absence is # 🚫 No `swarm.otel.scrapeTargets.bao` entry, and its absence is
# the deliverable rather than a tidy-up. That option is read only by the # deliberate. That option is read only by the SWARM collector, over
# SWARM collector, over loopback, so declaring the store there meant its # loopback, so a store declared there would have metrics exactly on the
# metrics existed exactly on the hosts that also ran swarm-otel and # hosts that also run swarm-otel and nowhere else — silently, since a
# nowhere else — silently, since a store with no entry looks identical to # store with no entry looks identical to one nothing scrapes. The scrape
# one nothing scrapes. The scrape moved into this container's own # runs in this container's own collector (see `receivers.prometheus`
# collector (see `receivers.prometheus` below), which travels with the # below), which travels with the store.
# container's own collector is confirmed.
# ⚠️ The one nginx exception to this file's header, and it is one because # ⚠️ 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 # 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 # 🩸 The principals that each log in with a leaf of their own, not as the
# `perHiveReaders` for what each grant is and why the first is per hive; # hive. See the block around `perHiveReaders` for what each grant is and
# the units are identical apart from the objects they # why the first is per hive; the units are identical apart from the
# write, so the shape lives in `readerPolicyUnit` and the difference is # objects they write, so the shape lives in `readerPolicyUnit` and the
# visible here in one line each. # 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-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; 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, # this line leaves a collector that validates, starts,
# reports healthy and forwards nothing. # reports healthy and forwards nothing.
directory = "/var/log/journal"; directory = "/var/log/journal";
# ⚠️ `--merge` stays from when this journal was host-linked # ⚠️ `--merge` is a guard: an unqualified
# and `journalctl --follow` (unqualified) found no entry for # `journalctl --follow` over a journal with no entry for the
# the current boot under that tree, crash-looping the # current boot (a host-linked one) crash-loops the receiver
# receiver every ~2s with "No journal boot entry found for # every ~2s with "No journal boot entry found for the
# the specified boot (+0)". The journal is local now, so the # specified boot (+0)". This journal is container-local, so
# current boot has an entry here too — kept rather than # the current boot has an entry and the flag changes nothing.
# dropped so the first deploy changes exactly one thing;
# revisit once delivery is confirmed.
merge = true; merge = true;
storage = "file_storage"; storage = "file_storage";
}; };
# The scrape that used to belong to the swarm collector, now # The store's scrape, run by this container's own collector.
# one hop shorter and one host less fussy — mara:
# *"move the existing scraper to the local collector"*.
# #
# ⚠️ `metrics_path` and `params` are STATED, and both have to # ⚠️ `metrics_path` and `params` are STATED, and both have to
# be: openbao serves no `/metrics` at all, and # be: openbao serves no `/metrics` at all, and
# `/v1/sys/metrics` answers JSON unless the format is asked # `/v1/sys/metrics` answers JSON unless the format is asked
# for. A scrape of the default path 404s and a scrape without # for. A scrape of the default path 404s and a scrape without
# the query parses as nothing — each reads as a dead exporter # the query parses as nothing — each reads as a dead exporter
# rather than as a wrong request, which is how the previous # rather than as a wrong request. The spelling matches
# owner of this scrape learned to spell it # ./swarm-otel.nix's `loopbackScrapeConfig`, the renderer for
# (./swarm-otel.nix's `loopbackScrapeConfig`, still the # every other service's target.
# renderer for every other service's target).
# #
# No credential, unlike the swarm collector's # No credential, unlike the swarm collector's
# `publishedScrapeTargets` jobs: the target is the loopback # `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 # something real — nothing whose trust store is built at build time can
# name it: `docs/swarm/ca.md::Distributing the root`. # name it: `docs/swarm/ca.md::Distributing the root`.
# #
# ⚠️ This root no longer issues the swarm's SERVICE certificates, and the # ⚠️ This root does not issue the swarm's SERVICE certificates; there is no
# `swarm-services-ca` sub-CA that used to sit under it is gone rather than # `swarm-services-ca` sub-CA under it. Those come out of the secret store's
# moved. Those come out of the secret store's own `pki` mount now, anchored # own `pki` mount, anchored on a root generated inside it (./swarm-bao.nix),
# on a root generated inside it (./swarm-bao.nix), and ./hive-tls.nix asks # and ./hive-tls.nix asks for them over the network instead of signing them
# for them over the network instead of signing them here. What is left here # here. This root has one job: the anchor each hive's own CA is issued under.
# is one job: the anchor each hive's own CA is issued under.
{ {
lib, lib,
config, config,

View file

@ -336,8 +336,8 @@ in
hostUnit = true; hostUnit = true;
enable = deployCfg.swarm-controller.enable; enable = deployCfg.swarm-controller.enable;
}) })
# The avatar moved with the forge-objects pass that uploads it: from # The avatar belongs to the forge-objects pass that uploads it, which
# hive-c0re's boot sweep to this daemon. # runs in this daemon.
(lib.mkRenamedOptionModule (lib.mkRenamedOptionModule
[ "services" "hyperhive" "c0re" "orgAvatarPng" ] [ "services" "hyperhive" "c0re" "orgAvatarPng" ]
[ "services" "hyperhive" "deploy" "swarm-controller" "configOrgAvatarPng" ] [ "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 # 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. # 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 # 🩸 Grafana's OWN leaf, not `clientCertFile` — the hive's. Bao matches a
# used to share. Bao matches a cert-auth role on the CN, so one leaf for four # cert-auth role on the CN, so one leaf shared by four readers would be ONE
# readers was ONE principal holding the union of four grants: this unit could # principal holding the union of four grants: this unit could read every
# read every agent credential in the swarm and every other service's OIDC # agent credential in the swarm and every other service's OIDC client
# client secret, when what it needs is the one path `storeSecretPath` names. # secret, when what it needs is the one path `storeSecretPath` names.
haveClientIdentity = haveClientIdentity =
baoDeploy.grafanaOidcClientCertFile != null && baoDeploy.grafanaOidcClientKeyFile != null; baoDeploy.grafanaOidcClientCertFile != null && baoDeploy.grafanaOidcClientKeyFile != null;
@ -377,7 +377,7 @@ in
and for two different reasons. Traces and Profiles front Tempo and for two different reasons. Traces and Profiles front Tempo
and Pyroscope, which this swarm does not run — a UI for a backend and Pyroscope, which this swarm does not run — a UI for a backend
that does not answer. Logs Drilldown is the subtler one: this 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 to it, but the app is Loki-only and no configuration here changes
that. Its volume views call `/loki/api/v1/index/volume`, which that. Its volume views call `/loki/api/v1/index/volume`, which
VictoriaLogs does not implement and answers "unsupported path VictoriaLogs does not implement and answers "unsupported path
@ -398,12 +398,12 @@ in
services.hyperhive.gateway.enable = lib.mkDefault true; services.hyperhive.gateway.enable = lib.mkDefault true;
services.hyperhive.gateway.dns.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 # Registering the client is NOT here: it has to happen on the host that
# host that runs authelia, and this whole block is gated on the host that # runs authelia, and this whole block is gated on the host that runs
# runs Grafana. ./glue-grafana-oidc-client.nix is where it moved to. # Grafana. ./glue-grafana-oidc-client.nix does it.
# This arm fires only where Grafana runs — this whole block is under # 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 # `swarm.authelia.url` is non-nullable, so every swarm has one and the
# OIDC block below always names a real endpoint. # OIDC block below always names a real endpoint.
assertions = [ assertions = [
@ -511,14 +511,13 @@ in
# authelia's host wrote it into, and that is true whether authelia is a # authelia's host wrote it into, and that is true whether authelia is a
# network away or in the container next door. # network away or in the container next door.
# #
# 🩸 A second unit here used to copy the plaintext out of authelia's host # 🩸 No second unit copies the plaintext out of authelia's host tree
# tree wherever the two were co-located, with the OIDC block gated on # where the two are co-located. The store exists so a host holds ONE
# either route being available. The ruling that deleted it: the store # out-of-band secret — its client certificate — and reads everything else
# exists so a host holds ONE out-of-band secret — its client certificate — # with it, so skipping the store when the producer is local would buy a
# and reads everything else with it, so skipping the store when the # round trip and cost a second delivery path, a second way for the file
# producer is local buys a round trip and costs a second delivery path, a # to be wrong, and a gate whose false arm is a Grafana with no login.
# second way for the file to be wrong, and a gate whose false arm was a # Recorded in docs/swarm/secrets.md.
# Grafana with no login. Recorded in docs/swarm/secrets.md.
# #
# Shaped after ./glue-queue-agent-credential.nix, because it is a store # 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 # 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 # property this option exists for — it should not arrive only
# once a plugin happens to be listed. # once a plugin happens to be listed.
# #
# - 🩸 and the one this comment used to miss: it also sets # - 🩸 it also sets upstream's `plugins.preinstall_disabled`
# upstream's `plugins.preinstall_disabled` (nixpkgs' # (nixpkgs'
# grafana.nix defaults it to `declarativePlugins != null`, # grafana.nix defaults it to `declarativePlugins != null`,
# saying preinstall "causes issues with declarativePlugins"). # saying preinstall "causes issues with declarativePlugins").
# Preinstall is what fetches the Drilldown apps on first # Preinstall is what fetches the Drilldown apps on first
# start, so claiming this directory silently removes them. # start, so claiming this directory silently removes them.
# That is why the option's default is not empty. # That is why the option's default is not empty.
# #
# The claim that used to sit here — "nothing is taken over on a # Taking this directory over is not harmless on a hive with no
# hive with no plugins, manual installation is impossible anyway" # plugins: that reasoning covers only plugins a PERSON installs,
# — reasoned only about plugins a PERSON installs. Upstream # and upstream installs some itself.
# installs some itself, and those are the ones that went missing.
declarativePlugins = deployCfg.grafana.plugins; declarativePlugins = deployCfg.grafana.plugins;
settings = { settings = {
@ -839,9 +837,7 @@ in
# Not conditional on anything, and the block below is not # Not conditional on anything, and the block below is not
# either, which is what keeps the two from ever disagreeing. # either, which is what keeps the two from ever disagreeing.
# Whether a password box sits on a public login page cannot # Whether a password box sits on a public login page cannot
# depend on which host happens to run authelia — and it used to, # depend on which host happens to run authelia.
# in a module where the OIDC block was gated and this line was
# not.
auth.disable_login_form = true; auth.disable_login_form = true;
# Emitted in every deployment, because SSO is a requirement of # 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 today) listens on, at `127.0.0.1`. Reached through the gateway at
`https://''${domain}/''${producerName}/`, same shape as a hive's `https://''${domain}/''${producerName}/`, same shape as a hive's
own receiver — a swarm-level producer may not share a host with own receiver — a swarm-level producer may not share a host with
this collector (mara, on the swarm-controller topology: "the this collector (the swarm services need not run on the same host
swarm services dont have to run on the same host as the swarm as the swarm controller), so loopback-only reachability is not a
controller"), so loopback-only reachability is not a supported supported shape here, as it is not for a hive.
shape here any more than it is for a hive.
Authenticated by `oidc/''${producerName}`, checking for the Authenticated by `oidc/''${producerName}`, checking for the
audience `services.hyperhive.swarm.controller.queueClientId` audience `services.hyperhive.swarm.controller.queueClientId`

View file

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

View file

@ -1,10 +1,10 @@
# Migration shim for `services.hyperhive.swarm.peers`, replaced by the # Migration shim for `services.hyperhive.swarm.peers`, replaced by the
# `swarm.hives` directory in ./swarm.nix. # `swarm.hives` directory in ./swarm.nix.
# #
# ⚠️ This could not be a `mkRenamedOptionModule`. `hives` is not `peers` # ⚠️ This is not a `mkRenamedOptionModule`. `hives` is not `peers`
# under a new name, it is **`peers` ∪ {self}**: the set gains a member no # 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 # 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. # default can re-root a meaning; neither can conjure a new member.
# #
# The whole module is self-contained and deletable — one file to remove # 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` # ⚠️ `services.hyperhive.swarm.ui` is not a namespace. Its two options,
# moved in an earlier slice and `package` in this one, and those were its # `enable` and `package`, live under `deploy.swarm-ui`, and the `swarm.ui`
# only two options — so both paths survive purely as renames in ./deploy.nix. # paths survive purely as renames in ./deploy.nix.
options.services.hyperhive.deploy.swarm-ui.package = lib.mkOption { options.services.hyperhive.deploy.swarm-ui.package = lib.mkOption {
type = lib.types.package; type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-ui"; 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 # ⚠️ Like the metrics store's, that endpoint is unauthenticated — which is
# why `listenAddress` above is loopback and why nothing reaches it except # why `listenAddress` above is loopback and why nothing reaches it except
# through the gateway, where the ingest route is authenticated. (This module # through the gateway, where the ingest route is authenticated. (This module
# does declare a vhost; the line that used to say otherwise was already # declares a vhost.)
# wrong before the collector started pushing through it.)
} }

View file

@ -33,8 +33,8 @@ let
# explain their own admission rules; the second says why they are not one # 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 # list. They are applied HERE rather than in the module that happens to
# consume them, because a hive name composes identifiers whatever else is # 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 # enabled — gating them on another module's `enable` would leave a swarm
# own `enable`, so a swarm without the collector had no check at all. # without that module with no check at all.
reservedNames = import ../reserved-names.nix; reservedNames = import ../reserved-names.nix;
reservedFragments = import ../reserved-hive-fragments.nix; reservedFragments = import ../reserved-hive-fragments.nix;
@ -213,7 +213,7 @@ in
# ⚠️ `lib.mkRemovedOptionModule` cannot do this job here, and # ⚠️ `lib.mkRemovedOptionModule` cannot do this job here, and
# neither of its two halves survives the move into a submodule: # neither of its two halves survives the move into a submodule:
# its `apply = throw` fires only when the value is READ, and # 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 # it); its `config.assertions` half would land on a submodule
# that declares no `assertions` option at all. It is a # that declares no `assertions` option at all. It is a
# top-level tool. The assertion below is the working shape, and # top-level tool. The assertion below is the working shape, and
@ -316,14 +316,14 @@ in
assertions = [ assertions = [
{ {
# An EMPTY `hives` fires this too, deliberately: since # 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 # 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 # 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 # here, because this host's own domain is now read out of the
# directory: without the entry `services.hyperhive.domain` is # directory: without the entry `services.hyperhive.domain` is
# null and the generic required-domain assertion in # null and the generic required-domain assertion in
# ./hive-network.nix would fire instead, naming an option the # ./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 # Guarded on `hiveName != null` so the required-hiveName
# assertion in ./hyperhive.nix is what fires for that case — # 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 # An error rather than a warning, deliberately. Re-declaring the
# removed option above is what stops "option does not exist"; on # 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 # its own it would turn a config that sets the removed field into
# that quietly evaluates with the setting ignored, which is a # one that quietly evaluates with the setting ignored, which is a
# worse answer than the unhelpful error it replaces. The operator # worse answer than an error. So this stays fatal until the field
# asked for a removal error, so this stays fatal until the field # is gone from the config.
# is gone from their config.
# #
# Wording follows `lib.mkRemovedOptionModule`'s, so it reads like # Wording follows `lib.mkRemovedOptionModule`'s, so it reads like
# every other removal the module system reports. # every other removal the module system reports.
@ -536,8 +535,8 @@ in
# swarm's token endpoint. The other two coordinates are this machine's — # 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 # 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 # 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 # the hive-c0re daemon's namespace, hence these live there rather than
# than a `deploy.statusPublish` of its own. # under a `deploy.statusPublish` of their own.
# #
# ⚠️ The all-or-nothing assertion above now spans both namespaces. That is # ⚠️ 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 # 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 # 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 # 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"; name = "an agent with a homeserver gets a main account and the matrix units";
ok = ok =
let let

View file

@ -101,7 +101,7 @@ let
ok = refusedWithoutIdentity controllerNoStore && !(refusedWithoutIdentity baoControllerHere); 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 # did not know the homeserver would create every agent without one. The
# default is the swarm's own `chat.` vhost, with no operator setting. # default is the swarm's own `chat.` vhost, with no operator setting.
name = "the controller is told the swarm's homeserver by default"; name = "the controller is told the swarm's homeserver by default";

View file

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

View file

@ -70,13 +70,11 @@ let
deploy.bao.grafanaOidcClientKeyFile = "/etc/pki/bao-grafana-oidc-key.pem"; deploy.bao.grafanaOidcClientKeyFile = "/etc/pki/bao-grafana-oidc-key.pem";
}; };
# A Grafana host holding no store identity. This used to be the shape the # A Grafana host holding no store identity. The module refuses it with an
# module went QUIET on — no OIDC block, a warning, and a container whose # assertion naming the two options to set, rather than going QUIET: no OIDC
# login form is off regardless, so no way in and nothing failed. It is kept # block can render and the container's login form is off regardless, so
# rather than deleted because the shape is still reachable by an operator; # there would be no way in and nothing would fail. Only the identity is
# what changed is the deliverable, from a warning nothing reads back to a # missing, so an arm below can name which refusal fired.
# refusal naming the two options to set. Only the identity is missing, so an
# arm below can name which refusal fired.
grafanaNoIdentity = hive { grafanaNoIdentity = hive {
deploy.grafana.enable = true; deploy.grafana.enable = true;
deploy.grafana.plugins = [ ]; deploy.grafana.plugins = [ ];
@ -104,9 +102,9 @@ let
) m.assertions; ) m.assertions;
cases = [ cases = [
{ {
# This fixture enables grafana and NOT authelia, which is the shape the # This fixture enables grafana and NOT authelia, so the login-form toggle
# login form used to stay enabled in: the toggle read "both services are # has to follow "grafana requires SSO" rather than "both services are on
# on this host" rather than "grafana requires SSO". Grafana ships an # this host". Grafana ships an
# `admin`/`admin` account and its vhost is on the public gateway, so a # `admin`/`admin` account and its vhost is on the public gateway, so a
# password box there is a way in whatever the topology. # 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"; 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 # 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 # 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 # deployment that has a secret store and no collector, rather than being
# them entirely, because they were assertions inside swarm-otel's own `mkIf`. # assertions inside swarm-otel's own `mkIf`.
# #
# ⚠️ `controllerCommonName` is overridden to a name containing NO reserved # ⚠️ `controllerCommonName` is overridden to a name containing NO reserved
# fragment. Its default (`swarm-controller`) contains `swarm` and is caught # 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 # refuses; see ./host-modules/deploy.nix. `package` and `bridgePackage` are
# ordinary options, so they do carry one. The two arms # ordinary options, so they do carry one. The two arms
# below have different jobs. `usersFile` tests the rename; the nats one tests # below have different jobs. `usersFile` tests the rename; the nats one tests
# that a reader repointed to the new namespace still renders the derived # that a reader pointed at the `deploy` namespace still renders the derived
# path, which is the failure this slice could actually have shipped — seven # path — seven of those reads go through an alias a path-shaped grep
# of those reads went through an alias a path-shaped grep cannot see. # cannot see.
autheliaOldPath = hive { autheliaOldPath = hive {
deploy.authelia.enable = true; deploy.authelia.enable = true;
deploy.nats.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 # 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 # `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 # does *not* say: no IdP address is given and none is derived from
# before that a co-location-derived guess. # co-location.
autheliaNotColocated = hive { }; autheliaNotColocated = hive { };
# The same swarm's IdP host, for the arm that the two agree. # The same swarm's IdP host, for the arm that the two agree.
autheliaColocated = hive { deploy.authelia.enable = true; }; 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 # 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. # 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 # ⚠️ `enable` is deliberately NOT set here: leaving it unset is what makes
# 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
# this fixture exercise the default rather than mask it. # this fixture exercise the default rather than mask it.
secretPublisherRemote = hive { secretPublisherRemote = hive {
deploy.authelia.enable = true; deploy.authelia.enable = true;
@ -125,9 +123,9 @@ let
&& !(lib.elem "swarm-grafana" (clients grafanaRemoteAuthelia)); && !(lib.elem "swarm-grafana" (clients grafanaRemoteAuthelia));
} }
{ {
# The collector's half of the same defect and the same fix: this used to # The collector's half of the same property: the client is not gated on
# be gated on `deploy.swarm-otel.enable`, so a split deployment # `deploy.swarm-otel.enable`, which would let a split deployment register
# registered the client nowhere and authelia minted nothing to publish. # it nowhere, so authelia would mint nothing to publish.
name = "the swarm's collector client is registered wherever authelia runs"; name = "the swarm's collector client is registered wherever authelia runs";
ok = ok =
let let

View file

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

View file

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

View file

@ -38,9 +38,9 @@ buildNpmPackage {
# Read from `../../frontend/npm-deps-hash`, the single source of # Read from `../../frontend/npm-deps-hash`, the single source of
# truth `./swarm-ui.nix` and `../checks.nix`'s `swarm-ui-typecheck` # truth `./swarm-ui.nix` and `../checks.nix`'s `swarm-ui-typecheck`
# also read — all three build from the one `frontend/package-lock.json`, # also read — all three build from the one `frontend/package-lock.json`,
# so one file keeps them from drifting independently (this used to be # so one file keeps them from drifting independently (a hardcoded copy
# a hardcoded copy per derivation, and only some of the three got # per derivation would let some of the three miss a lockfile change).
# updated the one time the lockfile changed). Regenerate with # Regenerate with
# `prefetch-npm-deps frontend/package-lock.json` and overwrite the # `prefetch-npm-deps frontend/package-lock.json` and overwrite the
# file whenever the lockfile changes. # file whenever the lockfile changes.
npmDepsHash = builtins.readFile ../../frontend/npm-deps-hash; 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 # Pure vendored Swagger UI 5.17.14 static dist — no hyperhive theming
# (see ./swagger-ui-theme.nix for that layer). Deliberately NOT sourced # (see ./swagger-ui-theme.nix for that layer). Deliberately NOT sourced
# via the Rust `utoipa-swagger-ui-vendored` crate / Cargo.lock (per the # via the Rust `utoipa-swagger-ui-vendored` crate / Cargo.lock: this is a
# operator's ask: a nix package that just contains the plain static # nix package that just contains the plain static dist, not fetched via
# dist, not fetched via cargo) — hive-c0re no longer needs to know this # cargo — hive-c0re does not need to know this exists, because the
# exists at all once the gateway hosts it directly. # gateway hosts it directly.
# #
# Sourced straight from the `swagger-ui-dist` npm package, which ships # Sourced straight from the `swagger-ui-dist` npm package, which ships
# exactly the built `dist/` files (no source, no build step needed) at # exactly the built `dist/` files (no source, no build step needed) at

View file

@ -21,7 +21,7 @@
# `{{config}}` placeholder its OWN build.rs # `{{config}}` placeholder its OWN build.rs
# injects (`replace_default_url_with_config` # injects (`replace_default_url_with_config`
# + `format_config`); hive-c0re doesn't serve # + `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 — # is baked in statically here instead —
# hand-verified byte-for-byte against what # hand-verified byte-for-byte against what
# `format_config` would produce, by applying # `format_config` would produce, by applying

View file

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