From b37c353f0eb4daa448425e09918911d1ffd1bd86 Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 30 May 2026 10:13:26 +0200 Subject: [PATCH] =?UTF-8?q?nix:=20hive-gateway=20v0=20=E2=80=94=20nginx=20?= =?UTF-8?q?in=20front=20of=20c0re=20(#609)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per mara's directive on #609: stand up a single nginx in its own nixos-container, serve the matrix GUI static dist there, proxy everything else to hive-c0re. v0 is HTTP-only; TLS / public-domain shape lands in follow-ups. New `nix/modules/hive-gateway.nix` declaring `containers.hive-gateway` modelled on `hive-forge`: - nixos-container running nginx, shares host netns - `location /matrix/` → static-serves `hyperhive.matrix.gui.package` (fluffychat-web by default) when `matrix.gui.enable` is true - `location /` → proxy_pass to `127.0.0.1:${dashboardPort}` with websocket + SSE upgrade headers + 1d read timeout Options (`hyperhive.gateway.*`): - `enable` (default `true`) — gateway on by default, opt out to bypass - `port` (default `80`) — nginx listen port on the host - `upstreamHost` / `upstreamPort` — c0re target, defaults to `127.0.0.1:${services.hive-c0re.dashboardPort}` - `openFirewall` (default `true`) — open the listen port - `localHostsEntry` (default `false`) — when true, adds an `/etc/hosts` entry mapping `hyperhive.domain` → `127.0.0.1` for local-dev / test loops without real DNS (per mara's spec) `hive-c0re.nix` updates: when gateway is enabled, skip wiring `HIVE_MATRIX_GUI_DIR` (gateway owns `/matrix/` now). When gateway is off, c0re's pre-existing matrix mount stays as the fallback. README: short "Optional" block introducing the gateway + the `localHostsEntry` knob. ```sh nix flake check --no-build nix build .#docs-host ``` End-to-end eval matrix: | gateway.enable | matrix.gui.enable | c0re HIVE_MATRIX_GUI_DIR | gateway container | | --- | --- | --- | --- | | true (default) | true | unset (gateway serves) | present | | true | false | unset | present, no /matrix | | false | true | set (c0re serves) | absent | | false | false | unset | absent | - TLS termination — separate follow-up once mara picks a story (self-signed-mkcert vs operator-provided certs) - Per-agent UI routing (`/agent//`) — depends on agent base-path support which is a frontend lift - Subdomain routing for `matrix.${hyperhive.domain}` — same-origin `/matrix/` is the v0 shape per mara ("leave everything else as is") Closes part of #609 (matrix GUI re-rooting onto nginx); leaves the issue open for the subdomain re-root + `.well-known/matrix/client` piece once the multi-host story matures. --- README.md | 12 ++- nix/modules/hive-c0re.nix | 13 ++- nix/modules/hive-gateway.nix | 186 +++++++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 nix/modules/hive-gateway.nix diff --git a/README.md b/README.md index c5f36cf8..1b638514 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Minimal `flake.nix` for a host that runs hive-c0re: nixosConfigurations.my-host = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ - hyperhive.nixosModules.default # hive-c0re + hive-forge in one import + hyperhive.nixosModules.default # hive-c0re + hive-forge + hive-gateway in one import ({ ... }: { services.hyperhive.enable = true; # services.hyperhive.c0re.operatorPronouns = "they/them"; # default: "she/her" @@ -78,6 +78,16 @@ manager container, and auto-rebuilds any container whose hyperhive rev goes stale. `claude-code` is unfree — hyperhive scopes the whitelist to itself, nothing for the operator to set. +Optional: set `services.hyperhive.gateway.enable = false;` to bypass +the default nginx in front. By default (`gateway.enable = true`) every +request hits a small nginx in its own nixos-container that proxies +to hive-c0re's dashboard on the upstream port; `/matrix/` is served +directly from `services.hyperhive.matrix.gui.package` when the matrix +GUI is on. v0 is HTTP-only (TLS lives in a follow-up); pair with +`services.hyperhive.gateway.localHostsEntry = true;` for local dev so +`http://` resolves to the host without +setting up real DNS. + Optional: set `services.hyperhive.c0re.preBuildAgentTemplates = true;` to pre-fetch the per-container system closures into your host's /nix/store as part of `nixos-rebuild`. First-agent-spawn then diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index 8d1630e6..19863253 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -23,6 +23,7 @@ in # is set before it can be enabled. imports = [ ./hive-forge.nix + ./hive-gateway.nix ./hive-matrix.nix ]; @@ -230,14 +231,12 @@ in # Agents poll this URL for Forgejo notifications. Derived from # services.hyperhive.forge.{domain,httpPort} so it tracks forge config changes. HIVE_FORGE_URL = "http://${config.services.hyperhive.forge.domain}:${toString config.services.hyperhive.forge.httpPort}"; - } - // lib.optionalAttrs config.services.hyperhive.matrix.gui.enable { - # Optional matrix-GUI static dist mounted at /matrix/ by the - # dashboard router (#607 v0). Pre-#15 / pre-nginx-front: this is - # the simplest same-origin shape — fluffychat-web ships as a - # static dist, no runtime daemon needed. - HIVE_MATRIX_GUI_DIR = "${config.services.hyperhive.matrix.gui.package}"; }; + # Matrix GUI static serving lives entirely on the hive-gateway + # nginx since #609 — when gateway is off the operator opts out + # of matrix-GUI serving entirely (mara on PR #620: "if you + # disable gateway you have to static host that yourself + # somewhere"). c0re no longer touches /matrix/. serviceConfig = { ExecStart = "${cfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --hyperhive-flake ${cfg.hyperhiveFlake} --dashboard-port ${toString cfg.dashboardPort} --operator-pronouns ${lib.escapeShellArg cfg.operatorPronouns} --context-window-tokens ${lib.escapeShellArg (builtins.toJSON cfg.contextWindowTokens)}"; Restart = "on-failure"; diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix new file mode 100644 index 00000000..c4c288c2 --- /dev/null +++ b/nix/modules/hive-gateway.nix @@ -0,0 +1,186 @@ +{ + pkgs, + lib, + config, + ... +}: +let + cfg = config.services.hyperhive.gateway; + hyperhiveDomain = config.services.hyperhive.domain; + matrixCfg = config.services.hyperhive.matrix; +in +{ + # Single nginx in front of every hyperhive surface (#609 / #15 v0). + # Lives in its own nixos-container (like hive-forge / hive-matrix) so + # the operator can opt out without touching the host's own nginx, and + # so the static-serve responsibility for the matrix GUI moves off + # hive-c0re's axum router. Shares host netns so `localhost` + # upstream resolution works without any port-forward dance. + # + # Routes (v0): + # `location /matrix/` → static-serve fluffychat-web dist (when + # `services.hyperhive.matrix.gui.enable` is true) + # `location /` → proxy_pass to hive-c0re's dashboard upstream + # + # Container name `hive-gateway` keeps hive-c0re's lifecycle scanner + # (which only sees `h-*`) out of the picture. State-free — nginx + # config lives in the nix store, no runtime persistence to manage. + + options.services.hyperhive.gateway = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Run hive-gateway — a single nginx in front of every hyperhive + surface. On by default: the gateway hosts the matrix GUI static + dist (when `services.hyperhive.matrix.gui.enable` is true) and + proxies everything else to hive-c0re's dashboard upstream. Set + `services.hyperhive.gateway.enable = false` to bypass nginx + entirely and reach hive-c0re directly on its dashboard port + (7000 by default). + + v0 is HTTP-only; TLS / public-domain shape is tracked + separately. + ''; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 80; + example = 8080; + description = '' + TCP port the gateway listens on. Default 80 (canonical web + port). nginx inside the container binds <1024 because the + container's init runs as root; if 80 is already taken on the + host (existing nginx, traefik, etc.) override to an unused + port like 8080 or move the conflicting service. + ''; + }; + + upstreamHost = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = '' + Host the gateway proxies non-static requests to. Defaults to + `127.0.0.1` because the gateway container shares the host + netns, so loopback resolves directly to hive-c0re. + ''; + }; + + upstreamPort = lib.mkOption { + type = lib.types.port; + default = 7000; + description = '' + TCP port the gateway proxies non-static requests to. Defaults + to `7000` (hive-c0re's out-of-the-box dashboard port). Operators + who change `services.hyperhive.c0re.dashboardPort` should set + `upstreamPort` to match — kept as a hardcoded default rather + than a cross-reference to keep this module's options eval + independent of c0re's option tree shape. + ''; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Open `port` in the host firewall. Off when the gateway should + only be reachable from inside the host (e.g. behind another + reverse proxy that handles TLS termination). + ''; + }; + + localHostsEntry = lib.mkOption { + type = lib.types.bool; + default = false; + example = true; + description = '' + Add an `/etc/hosts` entry mapping `services.hyperhive.domain` + to `127.0.0.1` on the host. Useful for local deployments + + tests where there's no real DNS for `services.hyperhive.domain` + but the operator (or browser-based tests) want to hit + `http://''${services.hyperhive.domain}` to exercise the + gateway shape. Off by default — operators running with real + DNS shouldn't have a stale `/etc/hosts` entry sticking + around. Requires `services.hyperhive.domain` to be set. + ''; + }; + }; + + config = lib.mkIf cfg.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. + ''; + } + ]; + + containers.hive-gateway = { + autoStart = true; + ephemeral = false; + # Share host netns — nginx then binds host-level ports directly, + # `localhost` upstream resolution reaches hive-c0re without any + # port-forward dance, and the firewall config below is the only + # layer that matters. + privateNetwork = false; + config = + { pkgs, ... }: + { + system.stateVersion = "26.05"; + services.nginx = { + enable = true; + recommendedProxySettings = true; + recommendedOptimisation = true; + virtualHosts."_" = { + listen = [ + { + addr = "0.0.0.0"; + port = cfg.port; + } + ]; + locations = + # Matrix GUI: when the operator has flipped both + # `services.hyperhive.matrix.enable` and `matrix.gui.enable` + # on, nginx serves fluffychat-web (or whatever override) + # as a static dist at `/matrix/`. fluffychat is a SPA + # — fall back to its index.html on deep links. + lib.optionalAttrs (matrixCfg.enable && matrixCfg.gui.enable) { + "/matrix/" = { + alias = "${matrixCfg.gui.package}/"; + extraConfig = '' + try_files $uri $uri/ /matrix/index.html; + ''; + }; + } + // { + # Everything else proxies to hive-c0re. Upgrade + # headers stay set so SSE (`/dashboard/stream`, + # `/events/stream`) + websocket (`/screen/ws`) + # endpoints keep working transparently. + "/" = { + proxyPass = "http://${cfg.upstreamHost}:${toString cfg.upstreamPort}"; + proxyWebsockets = true; + extraConfig = '' + proxy_buffering off; + proxy_read_timeout 1d; + ''; + }; + }; + }; + }; + }; + }; + + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.port ]; + }; + + networking.hosts = lib.mkIf (cfg.localHostsEntry && hyperhiveDomain != null) { + "127.0.0.1" = [ hyperhiveDomain ]; + }; + }; +}