diff --git a/docs/swarm.md b/docs/swarm.md index c255d44a..6138bd94 100644 --- a/docs/swarm.md +++ b/docs/swarm.md @@ -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 `.`, 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. 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 c7f6728e..fa836ad0 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; @@ -161,7 +172,33 @@ 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 + `.`. + ''; + } + { + 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";`). ''; } ]; diff --git a/nix/host-modules/hyperhive.nix b/nix/host-modules/hyperhive.nix index 9ea5b35e..6de59aee 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,35 @@ `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. + + **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 + `.`, 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. ''; };