nix: hive-gateway v0 — nginx in front of c0re (#609)
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/<name>/`) — 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.
This commit is contained in:
parent
1a839d45e3
commit
b37c353f0e
3 changed files with 203 additions and 8 deletions
12
README.md
12
README.md
|
|
@ -59,7 +59,7 @@ Minimal `flake.nix` for a host that runs hive-c0re:
|
||||||
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
|
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
modules = [
|
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.enable = true;
|
||||||
# services.hyperhive.c0re.operatorPronouns = "they/them"; # default: "she/her"
|
# 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
|
rev goes stale. `claude-code` is unfree — hyperhive scopes the
|
||||||
whitelist to itself, nothing for the operator to set.
|
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://<services.hyperhive.domain>` resolves to the host without
|
||||||
|
setting up real DNS.
|
||||||
|
|
||||||
Optional: set `services.hyperhive.c0re.preBuildAgentTemplates = true;`
|
Optional: set `services.hyperhive.c0re.preBuildAgentTemplates = true;`
|
||||||
to pre-fetch the per-container system closures into your host's
|
to pre-fetch the per-container system closures into your host's
|
||||||
/nix/store as part of `nixos-rebuild`. First-agent-spawn then
|
/nix/store as part of `nixos-rebuild`. First-agent-spawn then
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ in
|
||||||
# is set before it can be enabled.
|
# is set before it can be enabled.
|
||||||
imports = [
|
imports = [
|
||||||
./hive-forge.nix
|
./hive-forge.nix
|
||||||
|
./hive-gateway.nix
|
||||||
./hive-matrix.nix
|
./hive-matrix.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -230,14 +231,12 @@ in
|
||||||
# Agents poll this URL for Forgejo notifications. Derived from
|
# Agents poll this URL for Forgejo notifications. Derived from
|
||||||
# services.hyperhive.forge.{domain,httpPort} so it tracks forge config changes.
|
# 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}";
|
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 = {
|
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)}";
|
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";
|
Restart = "on-failure";
|
||||||
|
|
|
||||||
186
nix/modules/hive-gateway.nix
Normal file
186
nix/modules/hive-gateway.nix
Normal file
|
|
@ -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 ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue