types: let nix own the reserved-name blacklist

One list, in nix/reserved-names.nix, handed to everything that needs it
as HIVE_RESERVED_NAMES. Keeping it current becomes a config change
rather than a rebuild, and hive names and agent names -- one namespace
going forward -- are checked against the same file: swarm-otel.nix's
hand-written reservedOwners is gone.

Whitespace-separated rather than JSON, deliberately, unlike the
structured env vars beside it. Every entry is an Ident ([a-z0-9-]), so
whitespace cannot occur inside a name and the encoding is provably
lossless; JSON would mean either a parser dependency in a crate whose
purpose is to have none, or a copy of the parse in every consumer.

An UNSET variable is not "nothing is reserved". Both creation sites log
an error and return a warning saying the check did not run, so a
misconfigured deployment says so instead of silently accepting every
name. A blank value folds into unset: nix always renders a non-empty
list, so present-but-empty is a rendering fault, not a declaration.

Two guards whose subject moved out of their own file now assert their
own case is still in it, because a guard that can be retired by an edit
elsewhere is not a guard:

- swarm-otel.nix asserts reserved-names.nix still contains its
  swarmTierName.
- hive-sh4re's sentinel drift test PANICS when the variable is missing
  rather than skipping -- a drift test that quietly does nothing still
  reports green. checks.nix and devshell.nix both export it so CI and a
  local cargo test agree. Verified as a pair: with the variable set, 8
  tests pass; with it unset, exactly the 4 drift tests fail and the
  unrelated ones still pass.
This commit is contained in:
atlas 2026-08-27 15:15:33 +02:00 committed by mara
commit 27932ec631
10 changed files with 326 additions and 87 deletions

View file

@ -758,16 +758,35 @@ async fn create_agent(
//
// Collected rather than logged-and-dropped — see `CreateAgentResponse`.
let mut warnings = Vec::new();
if hive_types::is_reserved_name(&agent) {
// A protocol literal: the message layer already produces this name
// as a sender or recipient, so wakes from the component and
// messages from the agent become the same broker row.
let detail = format!(
"agent name {agent:?} is a reserved protocol name — messages from this agent will be \
indistinguishable from hyperhive's own; this will become an error"
);
tracing::warn!(agent = %agent, "create_agent: reserved name");
warnings.push(detail);
// The blacklist comes from nix via `HIVE_RESERVED_NAMES` — one file, read
// by this daemon, by hive-c0re and by the swarm collector's own owner
// assertion. An UNSET variable means this process was never told, which
// is not the same as "no name is reserved": staying quiet there would be
// a check that reports clean because it could not run.
let raw = hive_types::reserved_names_raw();
match raw.as_deref().map(hive_types::parse_reserved_names) {
None => {
tracing::error!(
var = hive_types::RESERVED_NAMES_ENV,
"create_agent: reserved-name check could not run — variable not set"
);
warnings.push(format!(
"the reserved-name check did not run: {} is unset, so {agent:?} was accepted \
without being checked against the protocol literals",
hive_types::RESERVED_NAMES_ENV
));
}
Some(reserved) if hive_types::is_reserved_name(&agent, &reserved) => {
// A protocol literal: the message layer already produces this
// name as a sender or recipient, so wakes from the component and
// messages from the agent become the same broker row.
tracing::warn!(agent = %agent, "create_agent: reserved name");
warnings.push(format!(
"agent name {agent:?} is a reserved protocol name — messages from this agent will \
be indistinguishable from hyperhive's own; this will become an error"
));
}
Some(_) => {}
}
let hive = hive_types::Ident::parse(&req.hive)