From ec565120af014b3e6cffdc61f58cab99c89ae2e5 Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 5 Aug 2026 14:45:12 +0200 Subject: [PATCH 1/3] feat(nix): derive the hive domain from a new swarm.domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `.`. 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. --- docs/swarm.md | 17 ++++++++++-- nix/host-modules/hive-network.nix | 5 +++- nix/host-modules/hyperhive.nix | 46 ++++++++++++++++++++++++++++++- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/docs/swarm.md b/docs/swarm.md index c255d44a..be4897ba 100644 --- a/docs/swarm.md +++ b/docs/swarm.md @@ -22,12 +22,23 @@ services.hyperhive = { domain = "pr1ma.example.com"; # machine-addressable DNS domain hiveName = "pr1ma"; # human display name (optional) swarm.name = "constellat1on"; # shared swarm display name (optional) + swarm.domain = "example.com"; # the swarm's DNS domain }; ``` -`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 +`domain` is required whenever hyperhive is enabled — eval fails with a +hint if it is unset. It drives `HYPERHIVE_HIVE_DOMAIN` in every +container so agents can form qualified labels +(`iris@pr1ma.example.com`). + +You can also *not* write it: with `swarm.domain` and `hiveName` set, +`domain` defaults to `.`, since every hive in a +swarm occupies its own sub-domain of it. That is a default and not a +rename — a hive that pins `domain` explicitly keeps exactly the value it +has today, which is the point: re-rooting where a value *comes from* +must not reinterpret the values already deployed. + +`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 diff --git a/nix/host-modules/hive-network.nix b/nix/host-modules/hive-network.nix index c7f6728e..218a88f8 100644 --- a/nix/host-modules/hive-network.nix +++ b/nix/host-modules/hive-network.nix @@ -161,7 +161,10 @@ in hive resolver is authoritative for `` 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 + `.`. ''; } ]; diff --git a/nix/host-modules/hyperhive.nix b/nix/host-modules/hyperhive.nix index 9ea5b35e..bb3f5b16 100644 --- a/nix/host-modules/hyperhive.nix +++ b/nix/host-modules/hyperhive.nix @@ -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,34 @@ `HYPERHIVE_HIVE_DOMAIN`; consumed by `hive-agent::identity::hive_domain()` for `@` qualified labels. + + Defaults to `.` 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 + `.` — set this plus + `services.hyperhive.hiveName` and a hive needs no domain of its + own. + + A swarm needs this set somewhere to address its hives uniformly; + it is left nullable so an existing single-hive deployment that + pins `services.hyperhive.domain` directly keeps evaluating + untouched. The only thing eval insists on is that the hive ends + up with a domain, by either route. ''; }; From 747f405c6f9f4d1c61c8b6513325917216d20de2 Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 5 Aug 2026 14:47:26 +0200 Subject: [PATCH 2/3] fix(nix): keep the domain-derived defaults total The required-domain assertion in hive-network.nix could not be reached: `forge.` and `matrix.` 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. --- docs/swarm.md | 8 ++++---- nix/host-modules/hive-forge/default.nix | 4 +++- nix/host-modules/hive-matrix.nix | 4 +++- nix/host-modules/hive-network.nix | 11 +++++++++++ 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/swarm.md b/docs/swarm.md index be4897ba..b2f4c388 100644 --- a/docs/swarm.md +++ b/docs/swarm.md @@ -38,10 +38,10 @@ rename — a hive that pins `domain` explicitly keeps exactly the value it has today, which is the point: re-rooting where a value *comes from* must not reinterpret the values already deployed. -`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` 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. diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index 518e22cf..c50532ef 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -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 = '' diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix index 419d5376..35c2bbdb 100644 --- a/nix/host-modules/hive-matrix.nix +++ b/nix/host-modules/hive-matrix.nix @@ -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 = '' diff --git a/nix/host-modules/hive-network.nix b/nix/host-modules/hive-network.nix index 218a88f8..4f460d01 100644 --- a/nix/host-modules/hive-network.nix +++ b/nix/host-modules/hive-network.nix @@ -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.`, `matrix.`) 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; From fbf375755176a587a36cb2019d6ad16097b2b546 Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 5 Aug 2026 15:07:11 +0200 Subject: [PATCH 3/3] feat(nix): require swarm.domain and hiveName MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 — `.` 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. --- docs/swarm.md | 44 ++++++++++++++++++------------- nix/host-modules/hive-network.nix | 23 ++++++++++++++++ nix/host-modules/hyperhive.nix | 26 +++++++++--------- 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/docs/swarm.md b/docs/swarm.md index b2f4c388..6138bd94 100644 --- a/docs/swarm.md +++ b/docs/swarm.md @@ -19,31 +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) - swarm.domain = "example.com"; # the swarm's DNS domain }; ``` -`domain` is required whenever hyperhive is enabled — eval fails with a -hint if it is unset. It drives `HYPERHIVE_HIVE_DOMAIN` in every -container so agents can form qualified labels -(`iris@pr1ma.example.com`). +`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.** -You can also *not* write it: with `swarm.domain` and `hiveName` set, -`domain` defaults to `.`, since every hive in a -swarm occupies its own sub-domain of it. That is a default and not a -rename — a hive that pins `domain` explicitly keeps exactly the value it -has today, which is the point: re-rooting where a value *comes from* -must not reinterpret the values already deployed. +`domain` is required too, but you no longer have to *write* it: it +defaults to `.`, 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. -`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. +`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. diff --git a/nix/host-modules/hive-network.nix b/nix/host-modules/hive-network.nix index 4f460d01..fa836ad0 100644 --- a/nix/host-modules/hive-network.nix +++ b/nix/host-modules/hive-network.nix @@ -178,6 +178,29 @@ in `.`. ''; } + { + 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 + (`.`), not only a display name. + Set it (`services.hyperhive.hiveName = "pr1ma";`). + ''; + } ]; # Virtual bridge — each agent container attaches a veth pair (isolation diff --git a/nix/host-modules/hyperhive.nix b/nix/host-modules/hyperhive.nix index bb3f5b16..6de59aee 100644 --- a/nix/host-modules/hyperhive.nix +++ b/nix/host-modules/hyperhive.nix @@ -85,11 +85,12 @@ in `services.hyperhive.hiveName` and a hive needs no domain of its own. - A swarm needs this set somewhere to address its hives uniformly; - it is left nullable so an existing single-hive deployment that - pins `services.hyperhive.domain` directly keeps evaluating - untouched. The only thing eval insists on is that the hive ends - up with a domain, by either route. + **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. ''; }; @@ -105,14 +106,13 @@ in 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 + `.`, 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. ''; };