diff --git a/docs/gateway.md b/docs/gateway.md index 313101ff..952c6cce 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -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://` (default `forge.`). 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:`. +`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://` (default +`forge.`; `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:` 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.` (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:` -via the shared host netns without a firewall hole. Flip to `true` when -you need: +default): agents reach Forgejo through the gateway (`forge.` 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://:/` directly (not behind the gateway). - External git clients that push/pull via SSH directly to the host. diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index 570d0e63..368a3a04 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -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.` 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. 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 `` 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) { diff --git a/nix/modules/hive-forge.nix b/nix/modules/hive-forge.nix index ca30c11f..cc99f7bc 100644 --- a/nix/modules/hive-forge.nix +++ b/nix/modules/hive-forge.nix @@ -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 `.` 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.`, 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." when set, else - "localhost"), or set a non-empty hostname like "forge.example.com" - or "git.internal". + "forge."), or set a non-empty + hostname like "forge.example.com" or "git.internal". ''; } ]; diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index bde98ac3..dd0310af 100644 --- a/nix/modules/hive-gateway.nix +++ b/nix/modules/hive-gateway.nix @@ -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 # `:`. 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 diff --git a/nix/modules/hive-matrix.nix b/nix/modules/hive-matrix.nix index 838bd08e..ed120fac 100644 --- a/nix/modules/hive-matrix.nix +++ b/nix/modules/hive-matrix.nix @@ -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 = '' diff --git a/nix/modules/hive-tls.nix b/nix/modules/hive-tls.nix index 2f38bdd6..0931e035 100644 --- a/nix/modules/hive-tls.nix +++ b/nix/modules/hive-tls.nix @@ -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.