refactor(#2015): drop redundant domain != null checks (domain is required) + update docs

This commit is contained in:
damocles 2026-06-26 20:43:54 +02:00 committed by mara
commit c8ae3371e1
6 changed files with 88 additions and 130 deletions

View file

@ -280,21 +280,18 @@ dashboard reach by design — the surface is privileged (approve /
deny / destroy) and must not be exposed without a real reverse
proxy in front.
## `HIVE_FORGE_URL`: domain via gateway for isolated agents, loopback for shared-netns
## `HIVE_FORGE_URL`: agents reach the forge via the gateway by domain
Agents poll `HIVE_FORGE_URL` for Forgejo notifications + run all
`hive-forge` calls against it. `hive-c0re.nix` sets this based on the
network isolation mode:
- **`network.isolateContainers = true`**: agents run in private netns and
get the bridge dnsmasq as their resolver. `HIVE_FORGE_URL` is set to
`http://<forge.domain>` (default `forge.<hive-domain>`). Agents resolve
the hostname via dnsmasq → bridge IP, then reach nginx on port 80 (bridge
firewall opens 80+443 when isolation is on). nginx proxies to forgejo — the
same path an operator browser takes, no raw port exposure needed.
- **`network.isolateContainers = false`** (default): agents share the host's
network namespace, so loopback reaches forgejo directly. `HIVE_FORGE_URL`
is `http://127.0.0.1:<forge.httpPort>`.
`hive-forge` calls against it. Network isolation is always on (the
shared-netns mode was removed), so agents run in a private netns and
can never reach the host's loopback. `hive-c0re.nix` sets
`HIVE_FORGE_URL` to `http://<forge.domain>` (default
`forge.<hive-domain>`; `services.hyperhive.domain` is required). Agents
get the bridge dnsmasq as their resolver, resolve the hostname →
bridge IP, then reach nginx on port 80 (the bridge firewall opens
80+443). nginx proxies to forgejo — the same path an operator browser
takes, no raw port exposure needed.
## hive-forge container shape
@ -306,10 +303,13 @@ the operator already runs on the host — separate systemd namespace,
separate state dir, separate port unless the operator deliberately
collides.
Container shares the host network namespace
(`privateNetwork = false`) so agents reach the forge at
`http://localhost:<httpPort>` without extra plumbing — nixos-container
is here for state + systemd-unit isolation, not network isolation.
The forge container shares the host network namespace
(`privateNetwork = false`), so forgejo's listeners look like a
host-side service — nixos-container is here for state + systemd-unit
isolation, not network isolation. Note this is the FORGE container;
agent containers are network-isolated and reach the forge through the
gateway by `forge.<domain>` (see `HIVE_FORGE_URL` above), not via the
host's loopback.
State lives at `/var/lib/nixos-containers/hive-forge/var/lib/forgejo/`
and survives container restart / host reboot. To wipe, destroy the
@ -336,9 +336,9 @@ via `-p 2222`). Port 22 is left alone on the host for openssh.
`openFirewall` (default **false**) controls whether `httpPort` and
`sshPort` are opened in the host firewall. Off by default (secure by
default): every agent container reaches Forgejo at `localhost:<httpPort>`
via the shared host netns without a firewall hole. Flip to `true` when
you need:
default): agents reach Forgejo through the gateway (`forge.<domain>` on
the bridge), not the raw port, so no firewall hole is needed. Flip to
`true` when you need:
- The operator's browser to reach `http://<host>:<httpPort>/` directly
(not behind the gateway).
- External git clients that push/pull via SSH directly to the host.

View file

@ -112,8 +112,9 @@ in
# The forge is mandatory — hive-c0re mirrors every agent's applied
# config repo into it and it's the canonical store for the meta flake
# + `internal/*` repos, so there's no enable toggle; it deploys with
# hyperhive itself. hive-matrix is opt-in (off by default) and asserts
# that `services.hyperhive.domain` is set before it can be enabled.
# hyperhive itself. hive-matrix is opt-in (off by default). All
# subsystems rely on `services.hyperhive.domain`, which is required
# (asserted in hive-network.nix) whenever hyperhive is enabled.
imports = [
./hive-ci.nix
./hive-forge.nix
@ -129,8 +130,8 @@ in
# Canonical hive DNS domain shared by every subsystem that needs a
# stable hostname. Typed nullOr (default null) so the option always
# exists, but it's REQUIRED whenever hyperhive is enabled — a config
# assertion (see the `config` block) fails eval when it's unset, since
# exists, but it's REQUIRED whenever hyperhive is enabled — an
# assertion in hive-network.nix fails eval when it's unset, since
# matrix bakes it in on first boot and the gateway/forge/agent URLs all
# derive from it (no safe default). Full identity-surface
# context (HYPERHIVE_HIVE_DOMAIN / HIVE_NAME / SWARM_NAME env-var
@ -831,51 +832,36 @@ in
config.services.hyperhive.swarm.wireguard.listenPort
];
assertions = [
{
# `domain` has no safe default: it's baked into the matrix server
# (tuwunnel) on first boot and drives the gateway/forge/agent URLs.
# Under mandatory network isolation agents can only reach the forge
# via `forge.<domain>` on the bridge — with no domain there's no
# reachable name. Changing it later is destructive
# (deletes Matrix history), so it must be set before first boot.
assertion = config.services.hyperhive.domain != null;
message = ''
services.hyperhive.domain must be set (e.g. "darkest.space").
It's baked into the matrix server on first boot and into the
gateway / forge / agent URLs, so hyperhive needs a stable domain
and there is no safe default. Agents reach the forge via
forge.<domain> on the bridge network, so without it the forge is
unreachable from containers. Changing the domain after first boot
is destructive (deletes Matrix history) pick it before initial
setup. See docs/gateway.md and docs/matrix.md.
'';
}
]
++ lib.optionals config.services.hyperhive.swarm.wireguard.enable [
{
assertion = config.services.hyperhive.swarm.wireguard.privateKeyFile != null;
message = ''
services.hyperhive.swarm.wireguard.enable requires
services.hyperhive.swarm.wireguard.privateKeyFile to be set.
Generate a key: wg genkey > /etc/wireguard/hive.key
'';
}
{
assertion = config.services.hyperhive.swarm.wireguard.address != "";
message = ''
services.hyperhive.swarm.wireguard.enable requires
services.hyperhive.swarm.wireguard.address to be set
(e.g. "10.100.0.1/24").
'';
}
]
++ lib.optionals config.services.hyperhive.otel.enable [
{
assertion = config.services.hyperhive.otel.endpoint != "";
message = "services.hyperhive.otel.enable is true but services.hyperhive.otel.endpoint is empty.";
}
];
# NB: `services.hyperhive.domain` is required when hyperhive is
# enabled — the canonical assertion lives in `hive-network.nix` (the
# hive resolver is authoritative for `<domain>` and agents reach the
# forge/matrix through the gateway by it). So everything below can
# treat `config.services.hyperhive.domain` as non-null.
assertions =
lib.optionals config.services.hyperhive.swarm.wireguard.enable [
{
assertion = config.services.hyperhive.swarm.wireguard.privateKeyFile != null;
message = ''
services.hyperhive.swarm.wireguard.enable requires
services.hyperhive.swarm.wireguard.privateKeyFile to be set.
Generate a key: wg genkey > /etc/wireguard/hive.key
'';
}
{
assertion = config.services.hyperhive.swarm.wireguard.address != "";
message = ''
services.hyperhive.swarm.wireguard.enable requires
services.hyperhive.swarm.wireguard.address to be set
(e.g. "10.100.0.1/24").
'';
}
]
++ lib.optionals config.services.hyperhive.otel.enable [
{
assertion = config.services.hyperhive.otel.endpoint != "";
message = "services.hyperhive.otel.enable is true but services.hyperhive.otel.endpoint is empty.";
}
];
systemd.services.hive-c0re = {
description = "hyperhive coordinator daemon";
@ -920,10 +906,11 @@ in
# behaviour (root auto-managed); true makes the sweep a no-op.
HYPERHIVE_RUTHLESS = lib.boolToString config.services.hyperhive.ruthless;
}
// lib.optionalAttrs (config.services.hyperhive.domain != null) {
// {
# Identity env vars threaded into c0re's own service env and
# forwarded by meta.rs into every sub-agent's harness env —
# full chain in docs/conventions.md::Hive identity.
# full chain in docs/conventions.md::Hive identity. `domain` is
# required (asserted in hive-network.nix), so it's always set.
HYPERHIVE_HIVE_DOMAIN = config.services.hyperhive.domain;
}
// lib.optionalAttrs (config.services.hyperhive.hiveName != null) {

View file

@ -69,13 +69,8 @@ in
domain = lib.mkOption {
type = lib.types.str;
default = if hyperhiveDomain != null then "forge.${hyperhiveDomain}" else "localhost";
defaultText = lib.literalExpression ''
if services.hyperhive.domain != null then
"forge.''${services.hyperhive.domain}"
else
"localhost"
'';
default = "forge.${hyperhiveDomain}";
defaultText = lib.literalExpression ''"forge.''${services.hyperhive.domain}"'';
example = "git.example.com";
description = ''
Public hostname for the forge. Doubles as both the forgejo
@ -83,10 +78,10 @@ in
gateway vhost server-name when `behindGateway = true`
(sub-domain routing see `docs/gateway.md`).
Defaults to `forge.''${services.hyperhive.domain}` when the
hive-domain is set (idiomatic sub-domain shape `forge`
labelled under the hive's bare domain), falling back to
`localhost` otherwise (direct-on-port behaviour).
Defaults to `forge.''${services.hyperhive.domain}` (idiomatic
sub-domain shape `forge` labelled under the hive's bare
domain). `services.hyperhive.domain` is required, so there's
always a domain to derive from.
Set to a full hostname (`git.example.com`,
`forge.internal.lan`, etc.) for a bespoke vhost shape the
@ -199,18 +194,17 @@ in
{
# `cfg.domain` can't be empty — would render `.<hive>` shaped
# garbage as both server_name (nginx wildcard catch-all) and
# /etc/hosts entry (invalid). Default rejects this case (lands
# `"localhost"` when hive-domain is unset), but operator-set
# empty strings should fail loud.
# /etc/hosts entry (invalid). The default derives a non-empty
# `forge.<domain>`, but an operator-set empty string should fail
# loud.
assertion = cfg.domain != "";
message = ''
services.hyperhive.forge.domain = "" is rejected. The
rendered URLs would be invalid (nginx wildcard catch-all
for an empty server_name, /etc/hosts rejects empty entries).
Either leave at default (auto-derives to
"forge.<services.hyperhive.domain>" when set, else
"localhost"), or set a non-empty hostname like "forge.example.com"
or "git.internal".
"forge.<services.hyperhive.domain>"), or set a non-empty
hostname like "forge.example.com" or "git.internal".
'';
}
];

View file

@ -385,14 +385,6 @@ in
config = lib.mkIf config.services.hyperhive.enable {
assertions = [
{
assertion = !cfg.localHostsEntry || hyperhiveDomain != null;
message = ''
services.hyperhive.gateway.localHostsEntry = true requires
services.hyperhive.domain to be set. Either pin a hostname
or leave `localHostsEntry` at its default of false.
'';
}
{
assertion = !(cfg.tls.acme.enable && cfg.tls.certDir != null);
message = ''
@ -632,10 +624,11 @@ in
'';
};
}
// lib.optionalAttrs (hyperhiveDomain != null) {
// {
# FluffyChat boot-config pre-fill so the client's
# `.well-known/matrix/client` lookup hits the
# right delegation endpoint.
# right delegation endpoint. `domain` is required, so
# this is always present.
"= /config.json" = {
extraConfig = ''
default_type application/json;
@ -678,7 +671,7 @@ in
# clients at `matrixCfg.gatewayHost` when set; falls back to direct
# `<hive>:<httpPort>`. CORS `*` per matrix spec. The `m.server`
# port-8448 carve-out is documented inline. See `docs/gateway.md`.
wellKnownLocations = lib.optionalAttrs (matrixCfg.enable && hyperhiveDomain != null) (
wellKnownLocations = lib.optionalAttrs matrixCfg.enable (
let
clientBaseUrl =
if matrixCfg.gatewayHost != null then
@ -1020,7 +1013,7 @@ in
# sub-domain modules that are on. `lib.unique` dedupes if any
# sub-domain happens to equal another. See `docs/gateway.md`
# ("Local dev").
networking.hosts = lib.mkIf (cfg.localHostsEntry && hyperhiveDomain != null) {
networking.hosts = lib.mkIf cfg.localHostsEntry {
"127.0.0.1" = lib.unique (
[ hyperhiveDomain ]
++ lib.optional (config.services.hyperhive.forge.behindGateway or false

View file

@ -149,13 +149,8 @@ in
gatewayHost = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = if hyperhiveDomain != null then "matrix.${hyperhiveDomain}" else null;
defaultText = lib.literalExpression ''
if services.hyperhive.domain != null then
"matrix.''${services.hyperhive.domain}"
else
null
'';
default = "matrix.${hyperhiveDomain}";
defaultText = lib.literalExpression ''"matrix.''${services.hyperhive.domain}"'';
example = "matrix.example.com";
description = ''
Public hostname for the matrix homeserver behind the gateway.
@ -294,25 +289,13 @@ in
};
config = lib.mkIf cfg.enable {
# serverName must exist (irrevocably embedded in user/room IDs);
# gatewayHost may not be "" (same footgun as forge.domain —
# nginx rejects an empty server_name). docs/matrix.md::Assertion
# rationale.
# `serverName` is irrevocably embedded in user/room IDs; it derives
# from `services.hyperhive.domain` (required, asserted in
# hive-network.nix) when not set explicitly, so no separate
# domain/serverName assertion is needed here. gatewayHost may not be
# "" (same footgun as forge.domain — nginx rejects an empty
# server_name). docs/matrix.md::Assertion rationale.
assertions = [
{
assertion = hyperhiveDomain != null || cfg.serverName != null;
message = ''
services.hyperhive.matrix.enable = true requires either:
- services.hyperhive.domain set to your host's canonical domain
(recommended; shared with forge / dashboard), or
- services.hyperhive.matrix.serverName set explicitly.
The matrix server_name is embedded into every user ID and
room ID on this homeserver it cannot be changed later
without losing every account and chat history. Pick a
stable hostname before enabling.
'';
}
{
assertion = cfg.gatewayHost == null || cfg.gatewayHost != "";
message = ''

View file

@ -12,11 +12,12 @@ let
# The host-managed hive CA is the trust anchor for self-signed mode.
# It is only stood up when the gateway actually serves a self-signed
# cert: a domain must be set (the leaf SANs derive from it) and the
# gateway must be in self-signed mode. The self-signed condition is the
# gateway module's single source of truth (`gateway.useSelfSigned`):
# true when neither an operator cert (`tls.certDir`) nor ACME is set.
active = hyperhiveCfg.enable && gatewayCfg.useSelfSigned && domain != null;
# cert: the gateway must be in self-signed mode. `domain` is required
# (asserted in hive-network.nix), so the leaf SANs always have a
# domain to derive from. The self-signed condition is the gateway
# module's single source of truth (`gateway.useSelfSigned`): true when
# neither an operator cert (`tls.certDir`) nor ACME is set.
active = hyperhiveCfg.enable && gatewayCfg.useSelfSigned;
in
{
# Host-side TLS trust root for the self-signed gateway mode.