Compare commits

...
Author SHA1 Message Date
atlas
fbf3757551 feat(nix): require swarm.domain and hiveName
Neither has a fallback worth having: a guessed swarm domain is a wrong
hostname that evaluates cleanly and deploys, which is worse than an
eval failure naming the one line an operator has to write. Upgrading
past this sets both, once.

Requiring them also makes the hive domain fully derived rather than
merely derivable — `<hiveName>.<swarm.domain>` now always resolves, so
an operator writes the swarm's address and this hive's label and never
writes the hive domain at all.

`hiveName` stops being display-only in the process: it is the leftmost
label of the domain the hive is addressed by, which the option text and
docs now say.

Each of the three required options asserts separately, so a missing one
names itself. A missing `swarm.domain` legitimately fails two of them —
its own, and `domain`, which can no longer derive — and nix reports all
failing assertions together, so the operator sees the whole set rather
than one at a time.
2026-08-05 15:56:49 +02:00
atlas
747f405c6f fix(nix): keep the domain-derived defaults total
The required-domain assertion in hive-network.nix could not be reached:
`forge.<domain>` and `matrix.<domain>` are evaluated while the
assertion list is, so an unset domain threw `cannot coerce null to a
string` naming one of those options instead of printing the message
that says which option to set.

Both defaults now fall back to a name under the reserved `.invalid`
TLD, which the assertion refuses to let out the door.
2026-08-05 15:56:49 +02:00
atlas
ec565120af feat(nix): derive the hive domain from a new swarm.domain
Every hive in a swarm occupies its own sub-domain of the swarm's, so
the hive domain is derivable rather than something each hive restates.
`services.hyperhive.swarm.domain` is new and nullable; the hive's own
`domain` keeps its existing required-ness and its existing assertion,
and gains a default of `<hiveName>.<swarm.domain>`.

Deliberately a default and not a rename: an alias would reinterpret the
domains hives have already deployed, while a default only fills in the
ones that never set one. Same reason there is no new assertion — the
existing message just names the derivation, so this adds a way to stop
failing rather than a way to fail.
2026-08-05 15:56:49 +02:00
5 changed files with 124 additions and 22 deletions

View file

@ -19,20 +19,37 @@ the additional config needed when the swarm spans multiple hosts.
```nix
services.hyperhive = {
domain = "pr1ma.example.com"; # machine-addressable DNS domain
hiveName = "pr1ma"; # human display name (optional)
swarm.domain = "example.com"; # required — the swarm's DNS domain
hiveName = "pr1ma"; # required — this hive's label in it
# domain = "pr1ma.example.com"; # derived from the two above
swarm.name = "constellat1on"; # shared swarm display name (optional)
};
```
`domain` is required when matrix federation is on (`matrix.enable`);
it drives `HYPERHIVE_HIVE_DOMAIN` in every container so agents can
form qualified labels (`iris@pr1ma.example.com`). `hiveName` and
`swarm.name` are purely display — they surface in the dashboard chrome
header and per-agent system prompts. Federated hives at different
domains can share a `swarm.name`; that it sits under `swarm` and
`hiveName` does not is the whole distinction — one names this hive, the
other names the group it belongs to.
`swarm.domain` and `hiveName` are **required** whenever hyperhive is
enabled; eval fails with a hint naming each. Neither is defaulted,
because a guessed value here is a wrong hostname that evaluates cleanly
and deploys — an eval failure asking the operator to write the address
down is the cheaper outcome. **Upgrading past this release means setting
both once.**
`domain` is required too, but you no longer have to *write* it: it
defaults to `<hiveName>.<swarm.domain>`, since every hive in a swarm
occupies its own sub-domain of it. A hive that pins `domain` explicitly
keeps exactly the value it has today — that's why this is a default and
not a rename: re-rooting where a value *comes from* must not reinterpret
the values already deployed.
`domain` drives `HYPERHIVE_HIVE_DOMAIN` in every container so agents can
form qualified labels (`iris@pr1ma.example.com`).
`swarm.name` is purely display — it surfaces in the dashboard chrome
header and per-agent system prompts, and federated hives at different
domains can share one. `hiveName` surfaces in the same places but is
*not* only display: it is the leftmost label of the hive's domain. That
`swarm.name` sits under `swarm` and `hiveName` does not is the whole
distinction — one names this hive, the other names the group it belongs
to.
See `docs/conventions.md` § Hive identity for the env-var chain
and `qualify()` / `qualified_label()` semantics.

View file

@ -120,7 +120,9 @@ in
domain = lib.mkOption {
type = lib.types.str;
default = "forge.${hyperhiveDomain}";
# Total on a null hive domain so the required-domain assertion in
# hive-network.nix is the thing that fires; see the comment there.
default = if hyperhiveDomain == null then "forge.invalid" else "forge.${hyperhiveDomain}";
defaultText = lib.literalExpression ''"forge.''${services.hyperhive.domain}"'';
example = "git.example.com";
description = ''

View file

@ -190,7 +190,9 @@ in
gatewayHost = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = "matrix.${hyperhiveDomain}";
# Total on a null hive domain so the required-domain assertion in
# hive-network.nix is the thing that fires; see the comment there.
default = if hyperhiveDomain == null then "matrix.invalid" else "matrix.${hyperhiveDomain}";
defaultText = lib.literalExpression ''"matrix.''${services.hyperhive.domain}"'';
example = "matrix.example.com";
description = ''

View file

@ -153,6 +153,17 @@ in
# hyperhive is enabled: the shared-netns mode was removed, so there
# is one mode (private netns behind the bridge).
(lib.mkIf config.services.hyperhive.enable {
# This message is only useful if an operator can actually reach
# it, and an assertion competes with every eager default that
# reads the value it guards: option defaults that interpolate the
# domain (`forge.<domain>`, `matrix.<domain>`) throw while the
# assertion list is being evaluated, so the operator sees
# `cannot coerce null to a string` naming an unrelated option
# instead of the sentence below. Those defaults therefore stay
# total, falling back to a name under the reserved `.invalid` TLD
# (RFC 2606) — a value this assertion then refuses to let out the
# door, and one that fails loudly at resolution rather than
# quietly working if it somehow did.
assertions = [
{
assertion = config.services.hyperhive.domain != null;
@ -161,7 +172,33 @@ in
hive resolver is authoritative for `<hive-domain>` and its
sub-domains, and agents reach the forge/matrix through the
gateway by that domain. Pin a hostname
(`services.hyperhive.domain = "example.com";`).
(`services.hyperhive.domain = "example.com";`), or set
`services.hyperhive.swarm.domain` and
`services.hyperhive.hiveName` and it is derived for you as
`<hiveName>.<swarm.domain>`.
'';
}
{
assertion = config.services.hyperhive.swarm.domain != null;
message = ''
hyperhive requires services.hyperhive.swarm.domain to be
set the DNS domain of the swarm this hive belongs to,
of which this hive occupies one sub-domain. There is no
fallback: a guessed value would be a wrong hostname that
evaluates cleanly and deploys. Set it
(`services.hyperhive.swarm.domain = "example.com";`)
with `hiveName` it also derives
`services.hyperhive.domain` for you.
'';
}
{
assertion = config.services.hyperhive.hiveName != null;
message = ''
hyperhive requires services.hyperhive.hiveName to be set
it is this hive's label within the swarm, and the leftmost
part of the domain it is addressed by
(`<hiveName>.<swarm.domain>`), not only a display name.
Set it (`services.hyperhive.hiveName = "pr1ma";`).
'';
}
];

View file

@ -4,8 +4,12 @@
# ./default.nix aggregator.
{
lib,
config,
...
}:
let
hiveCfg = config.services.hyperhive;
in
{
# The swarm's display name moved under `swarm` when the swarm-global
# settings were consolidated; the hive's own name and domain stayed put,
@ -32,7 +36,19 @@
# Hive identity (label + domain + display names).
options.services.hyperhive.domain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
# Every hive in a swarm lives at its own sub-domain of the swarm's,
# so this is derivable rather than something each hive repeats. It
# stays a DEFAULT and not a rename: an alias would reinterpret the
# domains hives have already deployed, whereas a default only fills
# in the ones that never set it. Null (both parts unset) keeps the
# existing "required" assertion in hive-network.nix as the single
# place this can fail.
default =
if hiveCfg.swarm.domain != null && hiveCfg.hiveName != null then
"${hiveCfg.hiveName}.${hiveCfg.swarm.domain}"
else
null;
defaultText = lib.literalExpression ''"''${hiveName}.''${swarm.domain}", or null when either is unset'';
example = "darkest.space";
description = ''
Canonical host domain for hyperhive subsystems that need a
@ -46,6 +62,35 @@
`HYPERHIVE_HIVE_DOMAIN`; consumed by
`hive-agent::identity::hive_domain()` for `<name>@<domain>`
qualified labels.
Defaults to `<hiveName>.<swarm.domain>` when both of those are
set, so a hive in a swarm does not restate its own address.
Setting this explicitly always wins.
'';
};
# Where the swarm lives. Declared beside the hive's own identity
# 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
# is genuinely new: nothing moved here, so there is no alias.
options.services.hyperhive.swarm.domain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "darkest.space";
description = ''
DNS domain of the wider swarm this hive belongs to. Each hive
occupies its own sub-domain of it, which is why
`services.hyperhive.domain` defaults to
`<hiveName>.<swarm.domain>` set this plus
`services.hyperhive.hiveName` and a hive needs no domain of its
own.
**Required** when `services.hyperhive.enable`, and deliberately
not defaulted: there is no fallback worth having. A guessed
swarm domain is a wrong hostname that evaluates cleanly and
deploys, which is worse than an eval failure telling an
operator to write down the one address their swarm answers to.
Upgrading past this costs one line, once.
'';
};
@ -61,14 +106,13 @@
example = "pr1ma";
description = ''
Human-readable name of this single-host hive instance.
Distinct from `services.hyperhive.domain` (the machine-
addressable DNS name): the domain may carry the hive name as
its leftmost label by convention, but this option is the
canonical readable identity. Exposed to agents as
`HYPERHIVE_HIVE_NAME`; surfaced in the dashboard chrome and
per-agent system prompt when set. Null falls back to the
default behaviour (chrome shows the domain, prompt doesn't
mention a hive name).
**Required** when `services.hyperhive.enable`. Distinct from
`services.hyperhive.domain` (the machine-addressable DNS name)
but no longer merely cosmetic: a hive occupies
`<hiveName>.<swarm.domain>`, so this is the label the hive is
*addressed* by as well as the one it is called. Exposed to
agents as `HYPERHIVE_HIVE_NAME`; surfaced in the dashboard
chrome and per-agent system prompt.
'';
};