diff --git a/nix/host-modules/lib/name-guards.nix b/nix/host-modules/lib/name-guards.nix deleted file mode 100644 index f9de705b..00000000 --- a/nix/host-modules/lib/name-guards.nix +++ /dev/null @@ -1,66 +0,0 @@ -# Assertions about names an operator chooses, so a module states WHAT is -# forbidden and not how to phrase the refusal. -{ lib }: -let - # One shape for both guards. Reports EVERY offender, not the first: fixing - # one per rebuild is the slowest possible way to learn a rule. - # - # Offenders come after their label rather than before it, so one name and - # five read the same — no English verb agrees with both. - guard = - { - option, - names, - forbidden, - hit, - problem, - forbiddenLabel, - why, - }: - let - offenders = lib.filter (n: lib.any (f: hit f n) forbidden) names; - quote = xs: lib.concatMapStringsSep ", " (x: "'${x}'") xs; - in - { - assertion = offenders == [ ]; - message = '' - ${option} ${problem}: ${quote offenders} - ${forbiddenLabel}: ${quote forbidden} - - ${why} - ''; - }; -in -{ - # No name may BE one of `reserved`. - mustNotEqual = - { - option, - names, - reserved, - why, - }: - guard { - inherit option names why; - forbidden = reserved; - hit = f: n: f == n; - problem = "has reserved name(s)"; - forbiddenLabel = "Reserved"; - }; - - # No name may CONTAIN one of `fragments`. - mustNotContain = - { - option, - names, - fragments, - why, - }: - guard { - inherit option names why; - forbidden = fragments; - hit = lib.hasInfix; - problem = "has name(s) containing a reserved word"; - forbiddenLabel = "Forbidden as substrings"; - }; -} diff --git a/nix/host-modules/swarm-otel.nix b/nix/host-modules/swarm-otel.nix index e4e40e17..baf64d13 100644 --- a/nix/host-modules/swarm-otel.nix +++ b/nix/host-modules/swarm-otel.nix @@ -72,13 +72,6 @@ let # would be an eval error rather than a silently missing guard. reservedOwners = import ../reserved-names.nix; - # Words no hive name may CONTAIN, because identifiers are composed from a - # hive name. That file explains why it is not merged into the list above. - reservedFragments = import ../reserved-hive-fragments.nix; - - nameGuards = import ./lib/name-guards.nix { inherit lib; }; - hiveNames = lib.attrNames hyperhiveCfg.swarm.hives; - # A published target is declared as ONE url, because that url is also the # audience its token is minted for — but prometheus wants the same fact in # three fields. Split it here rather than asking a service to state it @@ -665,30 +658,33 @@ in Put it back, or give this module a different swarmTierName. ''; } - (nameGuards.mustNotEqual { - option = "services.hyperhive.swarm.hives"; - names = hiveNames; - reserved = reservedOwners; - why = '' - The collector names components `/` with the hive name - as owner, and `//` resolves the clash silently: the swarm tier's - parts win, that hive's pipeline and `hive=` stamp disappear, and it - keeps pushing into a route that goes nowhere. Rename the hive. + { + # A hive whose name is one this module claims for itself collides in + # the collector's component namespace, and `//` resolves it silently: + # the swarm tier's parts win, that hive's `metrics/` pipeline + # disappears, and its `resource/` stamp goes with it — so the + # hive still connects and pushes into nothing, unlabelled. + # + # Checked rather than documented for the same reason the port range + # is: the names are all known at evaluation time, and the runtime + # symptom is a hive that looks healthy and reports no metrics, with + # nothing in any log naming the cause. + assertion = !lib.any (h: lib.elem h reservedOwners) (lib.attrNames hyperhiveCfg.swarm.hives); + message = '' + services.hyperhive.swarm.hives contains ${ + lib.concatMapStringsSep ", " (h: "'${h}'") ( + lib.filter (h: lib.elem h reservedOwners) (lib.attrNames hyperhiveCfg.swarm.hives) + ) + }, which the swarm collector reserves for its own pipelines. + + The collector names components `/` and uses the hive + name as the owner, so such a hive would silently replace the + swarm tier's parts and lose its own — it would keep accepting + pushes into a pipeline that routes nowhere. + + Rename the hive. Reserved: ${lib.concatMapStringsSep ", " (n: "'${n}'") reservedOwners}. ''; - }) - (nameGuards.mustNotContain { - option = "services.hyperhive.swarm.hives"; - names = hiveNames; - fragments = reservedFragments; - why = '' - Hive-scoped identifiers are composed from a hive name — - `hive-`, `hive--agent` — so a name containing one of - these produces an identifier that is also somebody else's. The - failure is a wrong grant rather than an error: the client - authenticates and receives another principal's permissions, and a - NATS denial arrives as a timeout. Rename the hive. - ''; - }) + } { # A published target that is not an `https://host/path` url. Without # this the split returns null and the failure surfaces as diff --git a/nix/reserved-hive-fragments.nix b/nix/reserved-hive-fragments.nix deleted file mode 100644 index f13b8e88..00000000 --- a/nix/reserved-hive-fragments.nix +++ /dev/null @@ -1,19 +0,0 @@ -# Words no HIVE name may CONTAIN. The substring half of `./reserved-names.nix`. -# -# Separate because the two differ in every dimension: subject (hive names only -# vs hive AND agent), matcher (substring vs equality), and admission rule (a -# fixed segment of a COMPOSED identifier vs a value some component emits). -# Folding them would widen the other one — containment over `forge` refuses -# `forgeworks`, which names no failure. -[ - # `hive-` — every hive's OIDC client id, and the string the queue's - # auth-callout responder strips to decide which hive a connection is. - "hive" - # `hive--agent` — the client its agent containers present. A hive named - # `foo-agent` mints exactly the id hive `foo`'s agents do. - "agent" - # Also an exact-match entry next door, for the collector's `/` - # components. Here because it is the word this swarm composes identifiers - # from everywhere. - "swarm" -] diff --git a/nix/reserved-names.nix b/nix/reserved-names.nix index 60ecefdf..f7fbb6b2 100644 --- a/nix/reserved-names.nix +++ b/nix/reserved-names.nix @@ -25,9 +25,6 @@ # `from`/`to`, or a component name the collector builds pipelines from — not a # word that merely looked risky. A name in here that nothing emits is a refusal # with no failure behind it. -# -# ⚠️ Matched by EQUALITY. Words forbidden *inside* a hive name live in -# `./reserved-hive-fragments.nix` — read its header before merging the two. [ # ---- message-layer senders ------------------------------------------- # The human at the dashboard: a broker recipient (the T4LK box sends