nix/hive-gateway: per-agent /agent/<name>/ routing through gateway (#15 v0)
Per mara on #14 (comment 9081): focused, purely additive to what's there, no TLS / no manager special cases, old `<host>:<port>/` path keeps working. Builds on iris's #731 (agent UI now serves document-relative URLs so it works under any nginx prefix). Mechanics: - New `services.hyperhive.gateway.agents` option (`listOf str`, default `[]`) lists sub-agent names to expose at `/agent/<name>/` through the gateway. - For each name, generate one `location /agent/<name>/` block that `proxy_pass`es to `http://127.0.0.1:<port>/`, where `<port>` is computed from the same FNV-1a hash hive-c0re uses internally (`lifecycle::agent_web_port`). - Trailing-slash pair on location + proxy_pass strips the `/agent/<name>` prefix on the upstream side — agent server receives `GET /`, `GET /api/state`, `GET /screen/ws`, etc. as if reached directly on its port. - `X-Forwarded-Prefix` set so the harness can build correct absolute URLs for cases where document-relative isn't enough. - `proxyWebsockets = true` + `proxy_buffering off` keeps SSE + WS endpoints working transparently. - Empty `cfg.agents` (default) → no per-agent blocks generated. - Manager not included — already gets `/` via the c0re upstream. FNV-1a hash replicated in nix to match `lifecycle::agent_web_port` line-for-line. Verified against rust output for 8 representative agent names: agent | nix | rust | match iris | 8178 | 8178 | ✓ atlas | 8304 | 8304 | ✓ argus | 8267 | 8267 | ✓ damocles | 8549 | 8549 | ✓ manager | 8000 | 8000 | ✓ (special case) dmatrix | 8266 | 8266 | ✓ triage | 8737 | 8737 | ✓ bitburner | 8658 | 8658 | ✓ Drift hazard documented in the let-block comment: if the rust constants change (MANAGER_PORT, WEB_PORT_BASE, WEB_PORT_RANGE, or the FNV-1a parameters), the nix copy needs a lockstep bump or gateway will proxy to wrong ports. Tracked in the option's description as a follow-up to single-source via `/var/lib/hyperhive/meta/topology.json` lib.importJSON OR runtime nginx-include written by c0re. Char-code lookup table covers `[a-z0-9_-]` — the current `hyperhive.user.name` alphabet. Names with other chars produce an eval-time error rather than a silent wrong hash. Verified: - `nix eval` on the locations attrset for [iris atlas argus damocles] → correct ports (matching rust impl) on each `/agent/<name>/` block - empty `cfg.agents` default → no per-agent blocks (`[ "/" ]` only) - full container toplevel builds cleanly with 7 agents + matrix on (`nixos-system-hive-gateway-26.05pre-git`) Sequencing per mara: this is #15 v0 (gateway-side per-agent routing, purely additive). #14 netns isolation follows once this soaks. Out of scope: TLS, manager special-case routing, per-agent unix sockets (mara: "at some point the agent servers will be domain sockets"), CORS workaround removal at `POST /answer-question/{id}`, gateway auth. Closes #15 v0.
This commit is contained in:
parent
4c30b52e48
commit
cb1a5cdbb8
1 changed files with 147 additions and 0 deletions
|
|
@ -8,6 +8,67 @@ let
|
||||||
cfg = config.services.hyperhive.gateway;
|
cfg = config.services.hyperhive.gateway;
|
||||||
hyperhiveDomain = config.services.hyperhive.domain;
|
hyperhiveDomain = config.services.hyperhive.domain;
|
||||||
matrixCfg = config.services.hyperhive.matrix;
|
matrixCfg = config.services.hyperhive.matrix;
|
||||||
|
|
||||||
|
# Per-agent web port, mirroring `hive-c0re::lifecycle::agent_web_port`
|
||||||
|
# exactly so the gateway and the harness agree on what port to talk
|
||||||
|
# to without a runtime contract. The manager is fixed at
|
||||||
|
# `MANAGER_PORT`; every sub-agent is `WEB_PORT_BASE + FNV-1a(name) %
|
||||||
|
# WEB_PORT_RANGE`. Pure + reproducible from just the name, matches
|
||||||
|
# the rust constants line-for-line (lifecycle.rs:60-79).
|
||||||
|
#
|
||||||
|
# **Drift hazard**: if the rust constants change (MANAGER_PORT,
|
||||||
|
# WEB_PORT_BASE, WEB_PORT_RANGE, MANAGER_NAME, or the FNV-1a
|
||||||
|
# parameters), this nix copy must change in lockstep — otherwise
|
||||||
|
# `/agent/<name>/` requests get proxied to the wrong port. There's
|
||||||
|
# no automated cross-check today (#15 v0 follow-up: have c0re emit
|
||||||
|
# the port mapping as `/var/lib/hyperhive/agent-ports.json` and
|
||||||
|
# have this module read it via `lib.importJSON`, single-sourcing
|
||||||
|
# the table).
|
||||||
|
agentWebPortLib =
|
||||||
|
let
|
||||||
|
# ASCII char → byte code lookup, limited to chars valid in agent
|
||||||
|
# names (lowercase alpha, digits, dash, underscore). Names with
|
||||||
|
# other chars are an eval-time error rather than a silent wrong
|
||||||
|
# hash. Add entries here if hyperhive ever loosens the naming
|
||||||
|
# constraint.
|
||||||
|
charCode = {
|
||||||
|
"0" = 48; "1" = 49; "2" = 50; "3" = 51; "4" = 52; "5" = 53;
|
||||||
|
"6" = 54; "7" = 55; "8" = 56; "9" = 57;
|
||||||
|
"-" = 45; "_" = 95;
|
||||||
|
"a" = 97; "b" = 98; "c" = 99; "d" = 100; "e" = 101;
|
||||||
|
"f" = 102; "g" = 103; "h" = 104; "i" = 105; "j" = 106;
|
||||||
|
"k" = 107; "l" = 108; "m" = 109; "n" = 110; "o" = 111;
|
||||||
|
"p" = 112; "q" = 113; "r" = 114; "s" = 115; "t" = 116;
|
||||||
|
"u" = 117; "v" = 118; "w" = 119; "x" = 120; "y" = 121;
|
||||||
|
"z" = 122;
|
||||||
|
};
|
||||||
|
|
||||||
|
webPortBase = 8100;
|
||||||
|
webPortRange = 900;
|
||||||
|
managerPort = 8000;
|
||||||
|
managerName = "manager";
|
||||||
|
|
||||||
|
# nix uses signed 64-bit ints; mask each step to u32 to mirror
|
||||||
|
# rust's `u32::wrapping_mul`. FNV-1a constants identical to the
|
||||||
|
# rust version (offset basis 2_166_136_261, prime 16_777_619).
|
||||||
|
fnv1aU32 =
|
||||||
|
name:
|
||||||
|
let
|
||||||
|
chars = lib.stringToCharacters name;
|
||||||
|
mask32 = h: lib.bitAnd h 4294967295;
|
||||||
|
step =
|
||||||
|
acc: c:
|
||||||
|
mask32 (
|
||||||
|
(lib.bitXor acc (
|
||||||
|
charCode.${c}
|
||||||
|
or (throw "agent name '${name}' contains char '${c}' outside the [a-z0-9_-] alphabet — hyperhive.user.name constraint mismatch?")
|
||||||
|
))
|
||||||
|
* 16777619
|
||||||
|
);
|
||||||
|
in
|
||||||
|
lib.foldl' step 2166136261 chars;
|
||||||
|
in
|
||||||
|
name: if name == managerName then managerPort else webPortBase + lib.mod (fnv1aU32 name) webPortRange;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Single nginx in front of every hyperhive surface (#609 / #15 v0).
|
# Single nginx in front of every hyperhive surface (#609 / #15 v0).
|
||||||
|
|
@ -117,6 +178,49 @@ in
|
||||||
around. Requires `services.hyperhive.domain` to be set.
|
around. Requires `services.hyperhive.domain` to be set.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
agents = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
|
example = [
|
||||||
|
"iris"
|
||||||
|
"atlas"
|
||||||
|
"argus"
|
||||||
|
"damocles"
|
||||||
|
];
|
||||||
|
description = ''
|
||||||
|
Sub-agent names to expose at `http(s)://<gateway>/agent/<name>/`
|
||||||
|
through the gateway (#15 v0). For each name in the list, the
|
||||||
|
gateway adds a `location /agent/<name>/` block that
|
||||||
|
`proxy_pass`es to `http://127.0.0.1:<port>/`, where `<port>` is
|
||||||
|
derived from the same `agent_web_port(name)` hash hive-c0re
|
||||||
|
uses internally (`lifecycle.rs` constants + FNV-1a, replicated
|
||||||
|
in this module's `let` block).
|
||||||
|
|
||||||
|
**Purely additive** — the old `http://<host>:<port>/` direct
|
||||||
|
reach still works in parallel; this just gives the operator a
|
||||||
|
single-origin route. Manager isn't included (no per-agent
|
||||||
|
prefix needed; manager already gets the `/` route via the
|
||||||
|
c0re upstream block).
|
||||||
|
|
||||||
|
Empty list (default) leaves the gateway in its pre-#15 shape:
|
||||||
|
no per-agent routes, only `/` (c0re) + `/matrix/` (fluffychat
|
||||||
|
when matrix is on) + `.well-known/matrix/*` (matrix
|
||||||
|
autodiscovery).
|
||||||
|
|
||||||
|
Maintenance: this list is currently operator-managed (the
|
||||||
|
gateway runs at the host level, the agent list lives in the
|
||||||
|
meta-flake at runtime, and the host's nix eval doesn't see
|
||||||
|
the meta-flake's agent dirs). Follow-up tracked at #15:
|
||||||
|
either auto-derive from
|
||||||
|
`/var/lib/hyperhive/meta/topology.json` via `lib.importJSON`
|
||||||
|
in the gateway module (gives a single source of truth at the
|
||||||
|
cost of an eval-time impurity), or have c0re write an nginx
|
||||||
|
snippet that the container `include`s + reloads on topology
|
||||||
|
change (decouples from rebuilds entirely; also handles the
|
||||||
|
eventual move to per-agent unix sockets).
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
|
@ -248,6 +352,49 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
//
|
||||||
|
# Per-agent UIs (#15 v0). One `/agent/<name>/`
|
||||||
|
# block per name in `services.hyperhive.gateway.agents`,
|
||||||
|
# proxying to the agent's harness web server on
|
||||||
|
# `127.0.0.1:<port>` where `<port>` comes from the
|
||||||
|
# `agentWebPortLib` FNV-1a hash (matches
|
||||||
|
# `lifecycle::agent_web_port` line-for-line).
|
||||||
|
#
|
||||||
|
# Trailing-slash pair (`/agent/<name>/` + `proxy_pass
|
||||||
|
# http://...:<port>/`) strips the `/agent/<name>`
|
||||||
|
# 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 `cfg.agents` list → empty attrset → no
|
||||||
|
# per-agent blocks; old `<host>:<port>/` direct reach
|
||||||
|
# still works.
|
||||||
|
builtins.listToAttrs (
|
||||||
|
builtins.map (name: {
|
||||||
|
name = "/agent/${name}/";
|
||||||
|
value = {
|
||||||
|
proxyPass = "http://127.0.0.1:${toString (agentWebPortLib name)}/";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
extraConfig = ''
|
||||||
|
proxy_set_header X-Forwarded-Prefix /agent/${name};
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_read_timeout 1d;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}) cfg.agents
|
||||||
|
)
|
||||||
// {
|
// {
|
||||||
# Everything else proxies to hive-c0re. Upgrade
|
# Everything else proxies to hive-c0re. Upgrade
|
||||||
# headers stay set so SSE (`/dashboard/stream`,
|
# headers stay set so SSE (`/dashboard/stream`,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue