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 ]; + }; + }; +}