diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index d73b5ce1..09b02a13 100644 --- a/nix/modules/hive-gateway.nix +++ b/nix/modules/hive-gateway.nix @@ -8,6 +8,31 @@ let cfg = config.services.hyperhive.gateway; hyperhiveDomain = config.services.hyperhive.domain; matrixCfg = config.services.hyperhive.matrix; + + # Per-agent port table for `/agent//` routing (#15 v0). Single- + # sourced from `cfg.agentPortsFile` (default + # `/var/lib/hyperhive/agent-ports.json`), written by hive-c0re on every + # topology change in shape `{ "": , ... }`. + # + # Read at deploy time via `builtins.fromJSON (builtins.readFile ...)` + # — pure eval (the file lives outside the nix store; nix copies the + # content into the store as a fixed-output dep). When the file is + # missing (fresh install before c0re has had a chance to write it), + # default to an empty map → no per-agent routes generated → gateway + # falls back to its pre-#15 shape. The container rebuilds on every + # `hivectl gateway-sync` (operator-initiated) or on the next + # `nixos-rebuild switch`, picking up whatever c0re has written + # since the last build. + # + # mara on #740 (comment 9295) + #15 (comment 9270): the gateway + # nginx container lives in system config (not meta), so it can't + # auto-rebuild from meta-flake events — the JSON file is what + # bridges the host's nix eval to the agent-lifecycle data c0re owns. + agentPortsTable = + if cfg.agentPortsFile == null || !builtins.pathExists cfg.agentPortsFile then + { } + else + builtins.fromJSON (builtins.readFile cfg.agentPortsFile); in { # Single nginx in front of every hyperhive surface (#609 / #15 v0). @@ -117,6 +142,42 @@ in around. Requires `services.hyperhive.domain` to be set. ''; }; + + agentPortsFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = "/var/lib/hyperhive/agent-ports.json"; + example = "/var/lib/hyperhive/agent-ports.json"; + description = '' + Path to a JSON file mapping sub-agent names to their web ports + for `/agent//` routing through the gateway (#15 v0). + Shape: `{ "": , ... }`. Written by hive-c0re on + every topology change (the rust side knows the canonical port + allocation via `lifecycle::agent_web_port`; the gateway just + reads what it's told). + + For each `: ` entry, the gateway adds a + `location /agent//` block that `proxy_pass`es to + `http://127.0.0.1:/`. Empty / missing file → no + per-agent routes generated → gateway falls back to its pre-#15 + shape (just `/` + matrix surfaces). + + **Purely additive**: the old `http://:/` direct + reach keeps working in parallel; this just gives the operator + a single-origin route. Manager isn't included in the map (no + per-agent prefix needed; manager already gets the `/` route + via the c0re upstream block). + + Set to `null` to disable per-agent routing entirely without + creating the file. Set to a custom path if the operator's c0re + writes the table elsewhere. + + **Rebuild trigger**: the gateway container picks up new entries + on the next `nixos-rebuild switch` (or `hivectl gateway-sync` + if that helper lands). c0re writes are not auto-applied to a + running gateway — see the follow-up in #15 for runtime nginx + include + reload + eventual per-agent unix sockets. + ''; + }; }; config = lib.mkIf cfg.enable { @@ -248,6 +309,46 @@ in ''; }; } + // + # Per-agent UIs (#15 v0). One `/agent//` + # block per `: ` entry in + # `agentPortsTable` (loaded from `cfg.agentPortsFile` + # — `/var/lib/hyperhive/agent-ports.json` by default, + # written by hive-c0re on every topology change). + # + # Trailing-slash pair (`/agent//` + `proxy_pass + # http://...:/`) strips the `/agent/` + # prefix on the upstream side, so the agent server + # receives `GET /` for the SPA root, `GET /api/state` + # for the API, `GET /screen/ws` for the websocket, etc. + # The agent's emitted asset URLs are document-relative + # (iris's #731) so they round-trip back through the + # gateway under the same prefix without the harness + # needing prefix-awareness. + # + # `X-Forwarded-Prefix` set so the harness can build + # correct absolute URLs for any case where relative + # isn't enough (server-emitted redirects, OG meta + # tags, etc.). + # + # SSE / websocket support via `proxyWebsockets = true` + # (same as the c0re `/` block below). + # + # Empty / missing `cfg.agentPortsFile` → empty table + # → no per-agent blocks; old `:/` direct + # reach still works. + lib.mapAttrs' (name: port: { + name = "/agent/${name}/"; + value = { + proxyPass = "http://127.0.0.1:${toString port}/"; + proxyWebsockets = true; + extraConfig = '' + proxy_set_header X-Forwarded-Prefix /agent/${name}; + proxy_buffering off; + proxy_read_timeout 1d; + ''; + }; + }) agentPortsTable // { # Everything else proxies to hive-c0re. Upgrade # headers stay set so SSE (`/dashboard/stream`,