From 2ff253e5206ba25d10aa1a76e59da7f5dc931336 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 16 Oct 2026 19:58:10 +0200 Subject: [PATCH] module-eval: drop 18 redundant cases and merge assertion-only hosts Refs #5379 --- nix/checks.nix | 1 - nix/module-eval/agent-forge-bao.nix | 14 -- nix/module-eval/agent-github-bao.nix | 4 - nix/module-eval/agent-plugins.nix | 28 +-- nix/module-eval/agent-runtime.nix | 12 - nix/module-eval/agent-user.nix | 19 -- nix/module-eval/bao-controller.nix | 43 ++-- nix/module-eval/bao-matrix-reader.nix | 7 - nix/module-eval/core-toggle.nix | 4 - nix/module-eval/name-guards.nix | 261 ++++++++-------------- nix/module-eval/nats-authelia.nix | 50 +---- nix/module-eval/nats-tls.nix | 9 +- nix/module-eval/shared.nix | 9 + nix/module-eval/snapshot-store.nix | 16 -- nix/module-eval/swarm-services-switch.nix | 4 - 15 files changed, 122 insertions(+), 359 deletions(-) diff --git a/nix/checks.nix b/nix/checks.nix index b9501e06..ec4ed5aa 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -200,7 +200,6 @@ in module-eval-agent-plugins = import ./module-eval/agent-plugins.nix { inherit pkgs self nixosSystem; inherit (pkgs) lib; - shared = moduleEvalShared; }; module-eval-agent-runtime = import ./module-eval/agent-runtime.nix { inherit pkgs self nixosSystem; diff --git a/nix/module-eval/agent-forge-bao.nix b/nix/module-eval/agent-forge-bao.nix index be6be7f4..5b72a293 100644 --- a/nix/module-eval/agent-forge-bao.nix +++ b/nix/module-eval/agent-forge-bao.nix @@ -113,16 +113,6 @@ let && c.RuntimeDirectoryPreserve == "yes" && c.UMask == "0377"; } - { - # Rename in only on a change: a reader never sees half a token, and the - # avatar watcher does not fire on every timer tick. - name = "the fetch swaps the token in by rename, only when it changed"; - ok = - let - s = (fetchUnit agentForgeBao).script; - in - lib.hasInfix "cmp -s" s && lib.hasInfix "mv -f" s; - } { # Every unit and the bash-task runner find the token through this. A # path, never the value. @@ -212,10 +202,6 @@ let && !(lib.hasInfix "rm -f \"$sidecar\"" s) && !(lib.hasInfix "forge-*" s); } - { - name = "the account fetch re-runs every two minutes"; - ok = agentForgeBao.systemd.timers.hive-agent-forge-accounts.timerConfig.OnUnitInactiveSec == "2min"; - } ]; in runGroup "agent-forge-bao" cases diff --git a/nix/module-eval/agent-github-bao.nix b/nix/module-eval/agent-github-bao.nix index 0f7af1e3..8786ebae 100644 --- a/nix/module-eval/agent-github-bao.nix +++ b/nix/module-eval/agent-github-bao.nix @@ -102,10 +102,6 @@ let name = "the fetch runs before the github poller starts"; ok = builtins.elem "hive-github-notify.service" (fetchUnit agentGithubBao).before; } - { - name = "the fetch re-runs every two minutes"; - ok = agentGithubBao.systemd.timers.hive-agent-github-token.timerConfig.OnUnitInactiveSec == "2min"; - } ]; in runGroup "agent-github-bao" cases diff --git a/nix/module-eval/agent-plugins.nix b/nix/module-eval/agent-plugins.nix index b4e332d7..81c654c7 100644 --- a/nix/module-eval/agent-plugins.nix +++ b/nix/module-eval/agent-plugins.nix @@ -6,7 +6,6 @@ lib, self, nixosSystem, - shared, }: let inherit @@ -24,16 +23,9 @@ let # `claudePlugins`'s additive-merge shape: a plain per-agent # definition must ADD to the module's own base-set definition rather than - # replacing it, while `lib.mkForce` must still replace the whole list - # outright — the two arms below plus the unset default (read directly off - # `bare`-shaped `agent { }`, no fixture of its own needed) are the three - # cases that shape has to hold. - agentPluginsDefault = shared.agentBare; - + # replacing it. agentPluginsAdded = agent { claudePlugins = [ "foo@bar" ]; }; - agentPluginsForced = agent { claudePlugins = lib.mkForce [ "foo@bar" ]; }; - # The de-dup arm: an agent that names a base-set entry explicitly must not # get it installed twice. agentPluginsDuplicate = agent { claudePlugins = [ "base@hyperhive" ]; }; @@ -41,17 +33,7 @@ let agentPlugins = machine: machine.services.hyperhive.agent.claudePlugins; cases = [ { - # Case 1 of the additive-merge shape: an agent that declares - # nothing gets exactly the module's base set, no more and no less. - name = "an agent with no claudePlugins definition gets exactly the base set"; - ok = - agentPlugins agentPluginsDefault == [ - "skill-creator@claude-plugins-official" - "base@hyperhive" - ]; - } - { - # Case 2: a plain per-agent definition ADDS to the base set (list-typed + # A plain per-agent definition ADDS to the base set (list-typed # options at equal priority concatenate) rather than replacing it — # the property the operator ruling asked for instead of `mkDefault`. # Sorted before comparing: concatenation order between two same- @@ -65,12 +47,6 @@ let "skill-creator@claude-plugins-official" ]; } - { - # Case 3: `lib.mkForce` is still the escape hatch — an operator who - # wants the base set gone, not extended, can still say so outright. - name = "an agent's mkForce claudePlugins replaces the base set outright"; - ok = agentPlugins agentPluginsForced == [ "foo@bar" ]; - } { # De-dup arm: an agent that names a base-set entry itself must not get # it installed twice — `lib.unique` at the JSON-render site, not the diff --git a/nix/module-eval/agent-runtime.nix b/nix/module-eval/agent-runtime.nix index 83dd4a31..5e3b2acb 100644 --- a/nix/module-eval/agent-runtime.nix +++ b/nix/module-eval/agent-runtime.nix @@ -125,18 +125,6 @@ let (subagent opencodeBao) ]; } - { - name = "an opencode agent's subagent daemon gets the agent's store identity"; - ok = - let - u = subagent opencodeBao; - in - u.serviceConfig.LoadCredential == [ "hive-agent-bao-server-ca" ] - && u.environment.HIVE_AGENT_NAME == "a1" - && u.environment.BAO_ADDR == "https://bao.t.local:8200" - && u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/current/cert" - && u.environment.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/current/key"; - } { name = "an opencode agent with no store hands its subagent daemon no store identity"; ok = diff --git a/nix/module-eval/agent-user.nix b/nix/module-eval/agent-user.nix index 6c5f5af3..4f3de42d 100644 --- a/nix/module-eval/agent-user.nix +++ b/nix/module-eval/agent-user.nix @@ -26,25 +26,6 @@ let (agentWith { services.hyperhive.agent.user.name = "iris"; }) .system.activationScripts.hive-agent-user-migrate.text; cases = [ - { - # hive-c0re creates the host work dir as hive-core; this activation is - # the only thing that lets the agent write to it. - name = "the agent's activation hands its own work dir to the agent user"; - ok = - lib.hasInfix ''if [ "$(stat -c %U "/agents/$userName/work")" != "$userName" ]; then'' migrate - && lib.hasInfix ''heal_own_dir "/agents/$userName/work"'' migrate; - } - { - # The helper is the only recursive chown of these dirs; it must hand the - # tree to the agent user and log a failure rather than discard it. - name = "the agent's activation hands state, harness and .claude to the agent user, logging failures"; - ok = - lib.hasInfix ''chown -hR "$userName:$userName" "$1" 2>&1'' migrate - && lib.hasInfix "could not chown $1 to $userName" migrate - && lib.hasInfix ''heal_own_dir "/agents/$userName/state"'' migrate - && lib.hasInfix ''heal_own_dir "/agents/$userName/harness"'' migrate - && lib.hasInfix ''heal_own_dir "$homeDir/.claude"'' migrate; - } { # hive-priv creates the host socket dir `0751 root` and never chowns it, # so this activation is the only thing that lets the harness bind there. diff --git a/nix/module-eval/bao-controller.nix b/nix/module-eval/bao-controller.nix index 982d6749..efca55be 100644 --- a/nix/module-eval/bao-controller.nix +++ b/nix/module-eval/bao-controller.nix @@ -46,15 +46,25 @@ let # files this host will never have. controllerNoStore = shared.controllerAlone; - # A deployment that pinned `gatewayHost` away from its own swarm-domain - # default (`hive-matrix-service.nix`'s own doc: "To use the per-hive name + # Two independent matrix settings no default could supply, each read into + # its own controller variable. + # + # `gatewayHost` pinned away from its own swarm-domain default + # (`hive-matrix-service.nix`'s own doc: "To use the per-hive name # ... instead, pin it here"). # Only this fixture can tell "the controller reads gatewayHost" apart from # "the controller re-derives gatewayHost's default itself" — both render # the same URL on every other fixture in this file. - controllerPinnedGateway = hive { + # + # An ignore list with two entries, so the case reading it also pins the + # separator the daemon splits on. + controllerPinnedMatrix = hive { deploy.swarm-controller.enable = true; swarm.matrix.gatewayHost = "matrix.hive.t.local"; + swarm.matrix.adminSync.ignoreUsers = [ + "@keep-a:t.local" + "@keep-b:t.local" + ]; }; # No matrix vhost for this swarm at all, so there is no homeserver URL @@ -64,16 +74,6 @@ let swarm.matrix.gatewayHost = null; }; - # An ignore list no default could supply, with two entries so the case below - # also pins the separator the daemon splits on. - controllerIgnoresAdmins = hive { - deploy.swarm-controller.enable = true; - swarm.matrix.adminSync.ignoreUsers = [ - "@keep-a:t.local" - "@keep-b:t.local" - ]; - }; - # The two authorities told apart. A deployment that self-signs both ends # points `clientCaFile` and `serverCaFile` at one file, so on the fixture # above the CA a hive is issued from and the CA the store is verified by are @@ -133,7 +133,7 @@ let # the former passes. name = "the controller's homeserver follows a pinned gatewayHost, not its own chat. default"; ok = - controllerPinnedGateway.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_MATRIX_HOMESERVER_URL + controllerPinnedMatrix.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_MATRIX_HOMESERVER_URL or null == "https://matrix.hive.t.local"; } { @@ -165,7 +165,7 @@ let # an account the sync would revoke. name = "the controller is told the operator admin sync's ignore list"; ok = - controllerIgnoresAdmins.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_MATRIX_ADMIN_SYNC_IGNORE_USERS + controllerPinnedMatrix.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_MATRIX_ADMIN_SYNC_IGNORE_USERS or null == "@keep-a:t.local @keep-b:t.local"; } { @@ -264,19 +264,6 @@ let && !builtins.elem "hive-client-ca.pem:/etc/pki/store-server-ca.pem" s.swarm-controller.serviceConfig.LoadCredential && m.deploy.swarm-controller.hiveClientCaFile == m.deploy.bao.clientCaFile; } - { - # Same two-CA fixture, for the same reason: the wrapper verifies the - # STORE, so it takes `serverCaFile`. On a self-signing deployment both - # options name one file and either would pass; here the client CA in that - # slot is a case this arm fails. - # - # ⚠️ The package itself stays off `PATH` — `wrapProgram` renames the real - # binary, so an unwrapped `bao` is unreachable rather than merely - # discouraged. Operator's instruction, and the last assertion is what - # keeps a later "install the package too" from quietly undoing it. - name = "the host gets a wrapped bao CLI"; - ok = baoWrapper != null; - } { name = "the wrapped bao CLI carries this store's address"; ok = lib.hasInfix "--set-default BAO_ADDR" baoWrapperCmd; diff --git a/nix/module-eval/bao-matrix-reader.nix b/nix/module-eval/bao-matrix-reader.nix index e8e41192..00f293ca 100644 --- a/nix/module-eval/bao-matrix-reader.nix +++ b/nix/module-eval/bao-matrix-reader.nix @@ -108,13 +108,6 @@ let && lib.hasInfix "exit 1" loginBranch && lib.hasInfix "exit 0" (lib.last (lib.splitString "bao kv get" u.script)); } - { - # The gate that decides this reader exists is the certificate rather - # than anything about agents: containers are created - # at runtime, so there is no static "this hive runs agents" fact to ask. - name = "a hive that names a client identity reads its agent queue credential"; - ok = baoRemoteReader.systemd.services ? swarm-bao-queue-agent; - } { # The reader's own grant covers `swarm/hives//*`; a path # outside it answers 403, not "no such key". This is the path diff --git a/nix/module-eval/core-toggle.nix b/nix/module-eval/core-toggle.nix index 00b48a83..c10f2b40 100644 --- a/nix/module-eval/core-toggle.nix +++ b/nix/module-eval/core-toggle.nix @@ -144,10 +144,6 @@ let in lib.length (lib.attrNames es) == 9 && !lib.any lib.id (lib.attrValues es); } - { - name = "a hive that has not opted into all-local runs no swarm controller"; - ok = !bare.services.hyperhive.deploy.swarm-controller.enable; - } { name = "the all-local mode turns the swarm controller on"; ok = allLocal.services.hyperhive.deploy.swarm-controller.enable; diff --git a/nix/module-eval/name-guards.nix b/nix/module-eval/name-guards.nix index 5795ce0b..c4a2fd4c 100644 --- a/nix/module-eval/name-guards.nix +++ b/nix/module-eval/name-guards.nix @@ -21,104 +21,74 @@ let runGroup ; - # The hive-name guards, with the collector explicitly OFF. That is the whole + # One host per guard. Each guard reports every offender in one message + # (`../host-modules/lib/name-guards.nix`), so one host carrying every + # colliding name still tells each name's arm apart: a name its arm misses + # is absent from the offenders line. + # + # Every fixture runs with the collector explicitly OFF. That is the whole # property: the guards live where `swarm.hives` is declared, so they run in a # deployment that has a secret store and no collector, rather than being # assertions inside swarm-otel's own `mkIf`. + + # Every cert-auth subject an operator can override, set to a name with no + # reserved fragment and taken by a hive. `controllerCommonName`'s default + # (`swarm-controller`) contains `swarm` and is caught by the substring guard + # whatever the cert-auth arm does, so the default could not tell the two + # apart. # - # ⚠️ `controllerCommonName` is overridden to a name containing NO reserved - # fragment. Its default (`swarm-controller`) contains `swarm` and is caught - # by the substring guard whatever the cert-auth arm does — so a fixture using - # the default could not tell the two apart, and the arm under test would pass - # on the neighbour's work. - hiveNamedAfterCertSubject = hive { + # The fixed subjects are a LIST, and a list with one consulted element and + # one dead one looks identical from the first element's case: each element + # needs its own name in the offenders line. matrix-ctl's grant names its + # leaves, not a whole prefix, so a hive that inherited a dead entry there + # would silently gain the ability to overwrite the swarm's matrix credential. + # + # 🩸 The two per-hive subjects have a different shape: those roles are + # written PER HIVE, so the subject a hive must not be is + # `-` rather than the prefix itself. Reserving only + # the prefix would leave the composed spelling free, and a hive taking it + # would present a leaf the other hive's role accepts — a hive reading another + # hive's queue credential, the exact widening the split exists to avoid. The + # collision is with the OTHER hive's role, hence `other` beside `qr-other` + # and `hl-other`. + hiveNamedAfterEverySubject = hive { deploy.swarm-otel.enable = false; - deploy.bao.controllerCommonName = "ctl"; - swarm.hives.ctl.domain = "ctl.t.local"; + deploy.bao = { + controllerCommonName = "ctl"; + secretPublisherCommonName = "pubctl"; + matrixCtlCommonName = "mintctl"; + grafanaOidcCommonName = "gfctl"; + otelOidcCommonName = "otctl"; + forwarderOidcCommonName = "fwctl"; + forgePublisherCommonName = "fpctl"; + granterCommonName = "grctl"; + queueAgentCommonNamePrefix = "qr"; + hiveLeafCommonNamePrefix = "hl"; + }; + swarm.hives = { + ctl.domain = "ctl.t.local"; + pubctl.domain = "p.t.local"; + mintctl.domain = "m.t.local"; + gfctl.domain = "g.t.local"; + otctl.domain = "o.t.local"; + fwctl.domain = "f.t.local"; + fpctl.domain = "fp.t.local"; + grctl.domain = "gr.t.local"; + other.domain = "other.t.local"; + qr-other.domain = "q.t.local"; + hl-other.domain = "hl.t.local"; + }; }; - # The control for both arms below: same shape, a roster nothing objects to. + # The control for every refusal below: same shape, a roster nothing objects + # to. It carries both sides of the identifier rule's edges — 63 characters, + # and digits and hyphens in every position (the rule has no + # leading-character restriction). hiveNamesAllLegal = hive { deploy.swarm-otel.enable = false; deploy.bao.controllerCommonName = "ctl"; - }; - - # The reserved subjects are a LIST, and a list with one consulted element and - # one dead one looks identical from the first element's case. This fixture - # collides with the SECOND, leaving the controller's at its default. - hiveNamedAfterPublisherSubject = hive { - deploy.swarm-otel.enable = false; - deploy.bao.secretPublisherCommonName = "pubctl"; - swarm.hives.pubctl.domain = "p.t.local"; - }; - - # The THIRD element of the same list, colliding on its own so neither of the - # two above can carry it. matrix-ctl's grant names its leaves, not a whole - # prefix, which is exactly why a dead entry here would be easy to miss: a - # hive that inherited it would not obviously break anything, it would - # silently gain the ability to overwrite the swarm's matrix credential. - hiveNamedAfterMatrixCtlSubject = hive { - deploy.swarm-otel.enable = false; - deploy.bao.matrixCtlCommonName = "mintctl"; - swarm.hives.mintctl.domain = "m.t.local"; - }; - - # The two OIDC-secret readers' subjects, fixed strings like the three above. - hiveNamedAfterGrafanaOidcSubject = hive { - deploy.swarm-otel.enable = false; - deploy.bao.grafanaOidcCommonName = "gfctl"; - swarm.hives.gfctl.domain = "g.t.local"; - }; - - hiveNamedAfterOtelOidcSubject = hive { - deploy.swarm-otel.enable = false; - deploy.bao.otelOidcCommonName = "otctl"; - swarm.hives.otctl.domain = "o.t.local"; - }; - - # The store forwarder's OIDC reader, the fifth fixed subject. - hiveNamedAfterForwarderOidcSubject = hive { - deploy.swarm-otel.enable = false; - deploy.bao.forwarderOidcCommonName = "fwctl"; - swarm.hives.fwctl.domain = "f.t.local"; - }; - - # The forge publisher's, the one writer of the controller's forge token. - hiveNamedAfterForgePublisherSubject = hive { - deploy.swarm-otel.enable = false; - deploy.bao.forgePublisherCommonName = "fpctl"; - swarm.hives.fpctl.domain = "fp.t.local"; - }; - - # The granter's, the one subject whose role may write every `swarm-*` grant. - hiveNamedAfterGranterSubject = hive { - deploy.swarm-otel.enable = false; - deploy.bao.granterCommonName = "grctl"; - swarm.hives.grctl.domain = "gr.t.local"; - }; - - # 🩸 A different shape from every fixture above: the queue-credential roles - # are written PER HIVE, so the subject a hive must not - # be is `-` rather than the prefix itself. Reserving - # only the prefix would leave the composed spelling free, and a hive taking it - # would present a leaf the other hive's role accepts — which is a hive reading - # another hive's queue credential, the exact widening the split exists to - # avoid. - # - # Two hives here, not one: the collision is with the OTHER hive's role. - hiveNamedAfterPerHiveReaderSubject = hive { - deploy.swarm-otel.enable = false; - deploy.bao.queueAgentCommonNamePrefix = "qr"; - swarm.hives.other.domain = "o.t.local"; - swarm.hives.qr-other.domain = "q.t.local"; - }; - - # The per-hive gateway-leaf subject, composed the same way. - hiveNamedAfterHiveLeafSubject = hive { - deploy.swarm-otel.enable = false; - deploy.bao.hiveLeafCommonNamePrefix = "hl"; - swarm.hives.other.domain = "o.t.local"; - swarm.hives.hl-other.domain = "hl.t.local"; + swarm.hives.${lib.concatStrings (lib.replicate 63 "a")}.domain = "l.t.local"; + swarm.hives."0-h-9".domain = "h.t.local"; }; hiveNameWithComposedWord = hive { @@ -128,40 +98,36 @@ let # The key characters are checked by the same module, so these run with the # collector off too. `a/b` adds a certificate-subject component; `../x` - # leaves a directory named after the hive. - hiveNameWithSlash = hive { + # leaves a directory named after the hive; 64 characters is one over the cap. + hiveNamesNotIdents = hive { deploy.swarm-otel.enable = false; - swarm.hives."a/b".domain = "a.t.local"; + swarm.hives = { + "a/b".domain = "a.t.local"; + "../x".domain = "x.t.local"; + ${lib.concatStrings (lib.replicate 64 "a")}.domain = "l.t.local"; + }; }; - hiveNameWithParentDir = hive { - deploy.swarm-otel.enable = false; - swarm.hives."../x".domain = "x.t.local"; - }; - - # Both sides of the length cap: 63 characters passes, 64 does not. - hiveNameAtLengthCap = hive { - deploy.swarm-otel.enable = false; - swarm.hives.${lib.concatStrings (lib.replicate 63 "a")}.domain = "l.t.local"; - }; - - hiveNameOverLengthCap = hive { - deploy.swarm-otel.enable = false; - swarm.hives.${lib.concatStrings (lib.replicate 64 "a")}.domain = "l.t.local"; - }; - - # Digits and hyphens in every position: the rule has no leading-character - # restriction. - hiveNameWithDigitsAndHyphens = hive { - deploy.swarm-otel.enable = false; - swarm.hives."0-h-9".domain = "h.t.local"; - }; + # The first line of a guard's message: the problem and its offenders. The + # lines after it list the forbidden values, which name a subject whether or + # not any hive took it. + offendersLine = a: lib.head (lib.splitString "\n" a.message); # Markers from `lib/name-guards.nix`'s `problem` strings. Matching the # problem rather than the `why` prose keeps the messages rewordable. equalityGuardFired = h: lib.any (a: !a.assertion && lib.hasInfix "has reserved name(s)" a.message) h.assertions; + # True when the equality guard names `name` as an offender. + equalityGuardRefuses = + h: name: + lib.any ( + a: + !a.assertion + && lib.hasInfix "has reserved name(s)" (offendersLine a) + && lib.hasInfix "'${name}'" (offendersLine a) + ) h.assertions; + fragmentGuardFired = h: lib.any ( @@ -181,44 +147,35 @@ let cases = [ { name = "a hive key containing a slash is refused, naming the key"; - ok = identGuardRefuses hiveNameWithSlash "a/b"; + ok = identGuardRefuses hiveNamesNotIdents "a/b"; } { name = "a hive key that climbs out of its directory is refused, naming the key"; - ok = identGuardRefuses hiveNameWithParentDir "../x"; + ok = identGuardRefuses hiveNamesNotIdents "../x"; } { name = "a hive key over 63 characters is refused"; - ok = identGuardFired hiveNameOverLengthCap; + ok = identGuardRefuses hiveNamesNotIdents (lib.concatStrings (lib.replicate 64 "a")); } { # Controls: without them an identifier guard that refuses everything # passes the three cases above. name = "63-character and digit-and-hyphen hive keys pass the identifier guard"; - ok = - !(identGuardFired hiveNameAtLengthCap) - && !(identGuardFired hiveNameWithDigitsAndHyphens) - && !(identGuardFired hiveNamesAllLegal); + ok = !(identGuardFired hiveNamesAllLegal); } { # `ctl` is in no deny list — it is reserved *because it is the subject a # cert-auth role accepts*, which is a value an operator sets, so a # literal deny entry could never have covered it. name = "a hive named after a cert-auth subject is refused, with the collector off"; - ok = - equalityGuardFired hiveNamedAfterCertSubject - && lib.any (a: !a.assertion && lib.hasInfix "'ctl'" a.message) hiveNamedAfterCertSubject.assertions; + ok = equalityGuardRefuses hiveNamedAfterEverySubject "ctl"; } { # Every cert-auth subject is reserved, not just the first one in the # list. Without this case the second element could be dead and the case # above would still pass. name = "a hive named after the secret publisher's subject is refused too"; - ok = - equalityGuardFired hiveNamedAfterPublisherSubject - && lib.any ( - a: !a.assertion && lib.hasInfix "'pubctl'" a.message - ) hiveNamedAfterPublisherSubject.assertions; + ok = equalityGuardRefuses hiveNamedAfterEverySubject "pubctl"; } { # And the third, for the reason the second one's comment gives one list @@ -226,11 +183,7 @@ let # has to register itself, and nothing but a case per element notices when # one forgets. name = "a hive named after matrix-ctl's subject is refused too"; - ok = - equalityGuardFired hiveNamedAfterMatrixCtlSubject - && lib.any ( - a: !a.assertion && lib.hasInfix "'mintctl'" a.message - ) hiveNamedAfterMatrixCtlSubject.assertions; + ok = equalityGuardRefuses hiveNamedAfterEverySubject "mintctl"; } { # The fourth and fifth, for the reason the case above gives: `certAuthCns` @@ -240,43 +193,25 @@ let # client secrets. name = "a hive named after either OIDC-secret reader's subject is refused too"; ok = - equalityGuardFired hiveNamedAfterGrafanaOidcSubject - && lib.any ( - a: !a.assertion && lib.hasInfix "'gfctl'" a.message - ) hiveNamedAfterGrafanaOidcSubject.assertions - && equalityGuardFired hiveNamedAfterOtelOidcSubject - && lib.any ( - a: !a.assertion && lib.hasInfix "'otctl'" a.message - ) hiveNamedAfterOtelOidcSubject.assertions; + equalityGuardRefuses hiveNamedAfterEverySubject "gfctl" + && equalityGuardRefuses hiveNamedAfterEverySubject "otctl"; } { # And the store forwarder's, for the same reason one element later. name = "a hive named after the store forwarder's OIDC-reader subject is refused too"; - ok = - equalityGuardFired hiveNamedAfterForwarderOidcSubject - && lib.any ( - a: !a.assertion && lib.hasInfix "'fwctl'" a.message - ) hiveNamedAfterForwarderOidcSubject.assertions; + ok = equalityGuardRefuses hiveNamedAfterEverySubject "fwctl"; } { # And the forge publisher's: a hive holding a leaf it accepts could # replace the token the controller provisions every forge object with. name = "a hive named after the forge publisher's subject is refused too"; - ok = - equalityGuardFired hiveNamedAfterForgePublisherSubject - && lib.any ( - a: !a.assertion && lib.hasInfix "'fpctl'" a.message - ) hiveNamedAfterForgePublisherSubject.assertions; + ok = equalityGuardRefuses hiveNamedAfterEverySubject "fpctl"; } { # And the granter's, whose role is root-equivalent: a hive holding a leaf # it accepts could grant itself anything. name = "a hive named after the bao granter's subject is refused too"; - ok = - equalityGuardFired hiveNamedAfterGranterSubject - && lib.any ( - a: !a.assertion && lib.hasInfix "'grctl'" a.message - ) hiveNamedAfterGranterSubject.assertions; + ok = equalityGuardRefuses hiveNamedAfterEverySubject "grctl"; } { # 🩸 The per-hive half, and the one a prefix-only reservation would miss: @@ -285,22 +220,14 @@ let # written for hive `other` — a leaf that reads a credential belonging to a # hive that is not it. name = "a hive named after another hive's per-hive reader subject is refused"; - ok = - equalityGuardFired hiveNamedAfterPerHiveReaderSubject - && lib.any ( - a: !a.assertion && lib.hasInfix "'qr-other'" a.message - ) hiveNamedAfterPerHiveReaderSubject.assertions; + ok = equalityGuardRefuses hiveNamedAfterEverySubject "qr-other"; } { # A hive taking this spelling would present a leaf hive `other`'s # gateway-leaf role accepts, and could ask the store for that hive's # certificate. The control is the legal roster below. name = "a hive named after another hive's gateway-leaf subject is refused"; - ok = - equalityGuardFired hiveNamedAfterHiveLeafSubject - && lib.any ( - a: !a.assertion && lib.hasInfix "'hl-other'" a.message - ) hiveNamedAfterHiveLeafSubject.assertions; + ok = equalityGuardRefuses hiveNamedAfterEverySubject "hl-other"; } { # Without this the case above proves nothing: an arm that fires for every @@ -320,7 +247,7 @@ let # pass while testing the arrangement they exist to rule out. name = "the guard fixtures really do have the collector disabled"; ok = - !hiveNamedAfterCertSubject.services.hyperhive.deploy.swarm-otel.enable + !hiveNamedAfterEverySubject.services.hyperhive.deploy.swarm-otel.enable && !hiveNamesAllLegal.services.hyperhive.deploy.swarm-otel.enable && !hiveNameWithComposedWord.services.hyperhive.deploy.swarm-otel.enable; } diff --git a/nix/module-eval/nats-authelia.nix b/nix/module-eval/nats-authelia.nix index edf3646d..b98af789 100644 --- a/nix/module-eval/nats-authelia.nix +++ b/nix/module-eval/nats-authelia.nix @@ -49,13 +49,7 @@ let deploy.nats.enable = true; }; - # The store's host with the hive's own reader pointed somewhere else. - natsOnStoreHostReaderMoved = hive { - deploy.bao.enable = true; - deploy.nats.enable = true; - deploy.bao.clientCertFile = "/etc/pki/h1.pem"; - deploy.bao.clientKeyFile = "/etc/pki/h1-key.pem"; - }; + inherit (shared) natsOnStoreHostReaderMoved; # A queue host holding only the hive's own store reader, placed by hand: no # store here, and no responder leaf named. @@ -309,17 +303,6 @@ let refusedWithoutIdentity natsWithHiveReaderOnly && natsWithHiveReaderOnly.services.hyperhive.deploy.nats.authBaoClientCertFile == null; } - { - # Reads the RENDERED settings, not the option: `calloutBlocks {…} // { - # … }` is a shallow merge, and a future edit that dropped or shadowed - # this key would still evaluate cleanly — the only reader that would - # notice is a publisher whose row exceeds upstream's much smaller - # default, and by then it is a dropped row, not an eval failure. - # Piggybacks on the queue-only fixture above, which already - # renders this container's full config. - name = "the queue's payload ceiling is set, not inherited from the server's default"; - ok = natsQueueOnly.containers.swarm-nats.config.services.nats.settings.max_payload == 8388608; - } { # What keeps a callout request inside one principal: the responder is # the callout account's only member, and no system account exists to @@ -485,16 +468,6 @@ let && lib.hasInfix "--agent-token-publish-subject '$$SWARM.agent-state.{agent}'" exec && lib.hasInfix "--store-cert-role swarm-nats-auth" exec; } - { - # Its own arm, like the two above: the icon write is a separate feature - # and dropping it must not hide behind the streams still being granted. - name = "the responder grants a verified agent its own icon key"; - ok = - let - exec = (responderOf natsQueueOnly).serviceConfig.ExecStart; - in - lib.hasInfix "--agent-token-publish-subject '$$KV.agent-icons.{agent}'" exec; - } { # The whole per-agent grant, compared as a list rather than by infix: a # wider subject added beside these (`$$JS.API.>`, another stream's name) @@ -722,21 +695,6 @@ let autheliaColocated.services.hyperhive.swarm.authelia.url == autheliaNotColocated.services.hyperhive.swarm.authelia.url; } - { - # …and the name it resolves through is a DOMAIN, not a host this config - # picked. `127.0.0.1` is what a co-location-derived default renders on - # the IdP host, the bridge address is what the queue's defaults render, - # and neither is an address a client may be handed: the domain is - # allowed to resolve differently in different places, which is the - # whole property. - name = "the swarm IdP address names no host address"; - ok = - let - url = autheliaNotColocated.services.hyperhive.swarm.authelia.url; - bridgeIp = autheliaNotColocated.services.hyperhive.network.bridgeIp; - in - !(lib.hasInfix "127.0.0.1" url) && !(lib.hasInfix "localhost" url) && !(lib.hasInfix bridgeIp url); - } { # The queue container's own journal forwarder, on the store's host and # on a queue host with no store and no swarm collector: it exports to @@ -773,12 +731,6 @@ let natsWithStoreIdentity ]; } - { - # The read grant the fetch relies on: the responder's role, the identity - # the forwarder logs in with, reads the queue client's secret. - name = "the queue container's store identity may read the forwarder's client secret"; - ok = lib.hasInfix "path \"secret/data/swarm/services/swarm-nats/oidc/client\" {" natsOnStoreHost.systemd.services.swarm-bao-nats-auth-policy.script; - } { # The far end: a receiver, authenticator, pipeline entry and gateway # route of its own, checking the queue client as audience. Rendered on diff --git a/nix/module-eval/nats-tls.nix b/nix/module-eval/nats-tls.nix index d0edc297..bc983e76 100644 --- a/nix/module-eval/nats-tls.nix +++ b/nix/module-eval/nats-tls.nix @@ -44,14 +44,7 @@ let deploy.nats.enable = true; }; - # The store's host with the hive's own reader pointed somewhere else. - natsOnStoreHostReaderMoved = hive { - deploy.bao.enable = true; - deploy.nats.enable = true; - deploy.nats.autoGenerateCallout = true; - deploy.bao.clientCertFile = "/etc/pki/h1.pem"; - deploy.bao.clientKeyFile = "/etc/pki/h1-key.pem"; - }; + inherit (shared) natsOnStoreHostReaderMoved; # A queue host holding only the hive's own store reader, placed by hand: no # store here, and no queue leaf named. diff --git a/nix/module-eval/shared.nix b/nix/module-eval/shared.nix index cf0ca91b..af9ae198 100644 --- a/nix/module-eval/shared.nix +++ b/nix/module-eval/shared.nix @@ -61,6 +61,15 @@ in deploy.nats.authPackage = pkgs.emptyDirectory; }; + # The queue on the store's host with the hive's own reader pointed + # somewhere else. + natsOnStoreHostReaderMoved = hive { + deploy.bao.enable = true; + deploy.nats.enable = true; + deploy.bao.clientCertFile = "/etc/pki/h1.pem"; + deploy.bao.clientKeyFile = "/etc/pki/h1-key.pem"; + }; + controllerAlone = hive { deploy.swarm-controller.enable = true; }; matrixNoBaoIdentity = hiveWithController { deploy.matrix.enable = true; }; diff --git a/nix/module-eval/snapshot-store.nix b/nix/module-eval/snapshot-store.nix index a2dc9af3..9aab4513 100644 --- a/nix/module-eval/snapshot-store.nix +++ b/nix/module-eval/snapshot-store.nix @@ -47,10 +47,6 @@ let snapshotStore.enable = true; }; - unit = store.systemd.services."swarm-snapshot-store@"; - - moduleSource = builtins.readFile ../host-modules/swarm-snapshot-store.nix; - socket = store.systemd.sockets.swarm-snapshot-store.socketConfig; refusedAsWildcard = @@ -85,18 +81,6 @@ let name = "a wildcard mesh address, IPv4 or IPv6, is refused"; ok = refusedAsWildcard storeOnWildcard && refusedAsWildcard storeOnWildcardV6; } - { - # Without it a stalled `btrfs receive` holds a root instance forever. - name = "the receiver unit has a runtime ceiling"; - ok = unit.serviceConfig.RuntimeMaxSec or null == "6h"; - } - { - # The script is a store path, so its text is not evaluable without - # building it; the module source is the nearest thing to read. A - # silent peer must not hold the root instance past the header read. - name = "the receiver script bounds its header read"; - ok = lib.hasInfix "read -r -t \${toString headerTimeoutSeconds} keyword agent" moduleSource; - } ]; in runGroup "snapshot-store" cases diff --git a/nix/module-eval/swarm-services-switch.nix b/nix/module-eval/swarm-services-switch.nix index 793c852b..a4a86a10 100644 --- a/nix/module-eval/swarm-services-switch.nix +++ b/nix/module-eval/swarm-services-switch.nix @@ -335,10 +335,6 @@ let name = "a named exposeHostPorts opens its bridge port on the host's own say-so"; ok = builtins.elem 5432 (bridgePortsOrNone exposedPortsNoHive); } - { - name = "the swarm UI claims the swarm apex wherever this host runs the swarm controller"; - ok = swarmUiHere.services.nginx.virtualHosts ? "t.local"; - } { name = "the old swarm UI package spelling sets the controller's UI package, which the vhost serves"; ok =