Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
Author SHA1 Message Date
atlas
7172176b4c swarm: extract the name guards, so the module just says what is forbidden
Her review: too much text, and an assertion helper instead of that much code
where the file should just say "this option cannot contain that".

`lib/name-guards.nix` gets `mustNotEqual` / `mustNotContain`; both call sites in
swarm-otel become four fields each — the option, the names, the list, and the
sentence explaining the failure. The message plumbing (find every offender,
quote them, name the list) is one shape shared by both.

Offenders are printed after their label rather than before it, because no
English verb agrees with both one name and five.

Converted the pre-existing equality assertion too. Leaving one hand-rolled
beside one helper-built is the worse of the two outcomes, and it is the same
guard.

Verified by evaluating both guards directly rather than only parsing: clean
rosters pass, `foo-agent`/`beehive` fail containment, `swarm` fails equality,
and `forgeworks`/`operator-hq` still pass — the control that keeps the two
lists from being merged.
2026-08-31 18:50:15 +02:00
atlas
cdaf5824ed swarm: refuse hive names that CONTAIN a word identifiers are composed from
Hive-scoped identifiers are built by joining a hive name with fixed words —
`hive-<name>`, `hive-<name>-agent` — so a hive called `foo-agent` produces
exactly the id hive `foo`'s agent containers produce. The queue's responder
resolves it as the agents, and that hive silently receives an agent grant
instead of its own; a NATS denial reaches a client as a timeout, so nothing
names the cause. Equality cannot see this: the two NAMES differ, only the
composed strings collide.

Deliberately a second file rather than three more entries in
`reserved-names.nix`. That list is matched by equality against a whole name and
every entry is a value some component emits; applying containment to it would
refuse `forgeworks` and `operator-hq` for failures that do not exist. The two
lists differ in subject (hive-only vs both), matcher (substring vs equality)
and admission rule, and each file's header says so.

No Rust change: `HIVE_RESERVED_NAMES` answers "may an AGENT be called this",
which stays an equality question — an agent name is never composed into these
identifiers.

Measured against the predicate rather than assumed:

  pr1ma=ok alpha=ok forgeworks=ok operator-hq=ok
  foo-agent=REFUSED beehive=REFUSED swarm=REFUSED myswarmx=REFUSED

The four `ok` cases are the control: `forgeworks` and `operator-hq` are exactly
what a merged list would have refused.
2026-08-31 18:50:15 +02:00
4 changed files with 118 additions and 26 deletions

View file

@ -0,0 +1,66 @@
# 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";
};
}

View file

@ -72,6 +72,13 @@ 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
@ -658,33 +665,30 @@ in
Put it back, or give this module a different swarmTierName.
'';
}
{
# 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/<name>` pipeline
# disappears, and its `resource/<name>` 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 `<kind>/<owner>` 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.mustNotEqual {
option = "services.hyperhive.swarm.hives";
names = hiveNames;
reserved = reservedOwners;
why = ''
The collector names components `<kind>/<owner>` 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.
'';
}
})
(nameGuards.mustNotContain {
option = "services.hyperhive.swarm.hives";
names = hiveNames;
fragments = reservedFragments;
why = ''
Hive-scoped identifiers are composed from a hive name —
`hive-<name>`, `hive-<name>-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

View file

@ -0,0 +1,19 @@
# 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-<name>` — 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-<name>-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 `<kind>/<owner>`
# components. Here because it is the word this swarm composes identifiers
# from everywhere.
"swarm"
]

View file

@ -25,6 +25,9 @@
# `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