mara on PR #740 comment 9295: "we decided to go with the json" (issue #15 comment 9270: "nginx container lives in system config, so it cannot be just rebuilt from meta flake. go for the json file the c0re writes"). Drops: - `cfg.agents` listOf str option - Replicated FNV-1a hash + char-code table + manager-port special case - Drift-hazard comment (no more rust↔nix constant sync) Adds: - `cfg.agentPortsFile = "/var/lib/hyperhive/agent-ports.json"` (default, nullable to disable) — path to a JSON map of `{ "<name>": <port> }` written by hive-c0re on every topology change. - `agentPortsTable` reads the file at eval time via `builtins.fromJSON (builtins.readFile path)`, guarded by `builtins.pathExists` so a missing file gracefully defaults to `{}`. - Per-agent locations generated via `lib.mapAttrs'` over the table — one location block per entry; empty table → empty attrset → no per-agent blocks, pre-#15 shape. Rust-side dependency: hive-c0re needs to emit the JSON file on every topology change. Coordinating with damocles via a separate ping — the nix side ships now with safe defaults (missing file = no routes, no behavior change vs main). Verified: - nix eval with `/tmp/test-agent-ports.json` → 4 per-agent blocks at correct ports (8178 iris, 8267 argus, 8304 atlas, 8549 damocles) - nix eval with nonexistent file → only `/` location (graceful default) - full container toplevel builds clean with matrix on Empty file case mirrors the previous empty-list default — purely additive, old `<host>:<port>/` direct reach untouched, no per-agent blocks until c0re writes the JSON. Operator can also `null` the option to disable entirely.
This commit is contained in:
parent
cb1a5cdbb8
commit
09c1abe5b4
1 changed files with 71 additions and 117 deletions
|
|
@ -9,66 +9,30 @@ let
|
||||||
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`
|
# Per-agent port table for `/agent/<name>/` routing (#15 v0). Single-
|
||||||
# exactly so the gateway and the harness agree on what port to talk
|
# sourced from `cfg.agentPortsFile` (default
|
||||||
# to without a runtime contract. The manager is fixed at
|
# `/var/lib/hyperhive/agent-ports.json`), written by hive-c0re on every
|
||||||
# `MANAGER_PORT`; every sub-agent is `WEB_PORT_BASE + FNV-1a(name) %
|
# topology change in shape `{ "<name>": <port>, ... }`.
|
||||||
# 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,
|
# Read at deploy time via `builtins.fromJSON (builtins.readFile ...)`
|
||||||
# WEB_PORT_BASE, WEB_PORT_RANGE, MANAGER_NAME, or the FNV-1a
|
# — pure eval (the file lives outside the nix store; nix copies the
|
||||||
# parameters), this nix copy must change in lockstep — otherwise
|
# content into the store as a fixed-output dep). When the file is
|
||||||
# `/agent/<name>/` requests get proxied to the wrong port. There's
|
# missing (fresh install before c0re has had a chance to write it),
|
||||||
# no automated cross-check today (#15 v0 follow-up: have c0re emit
|
# default to an empty map → no per-agent routes generated → gateway
|
||||||
# the port mapping as `/var/lib/hyperhive/agent-ports.json` and
|
# falls back to its pre-#15 shape. The container rebuilds on every
|
||||||
# have this module read it via `lib.importJSON`, single-sourcing
|
# `hivectl gateway-sync` (operator-initiated) or on the next
|
||||||
# the table).
|
# `nixos-rebuild switch`, picking up whatever c0re has written
|
||||||
agentWebPortLib =
|
# since the last build.
|
||||||
let
|
#
|
||||||
# ASCII char → byte code lookup, limited to chars valid in agent
|
# mara on #740 (comment 9295) + #15 (comment 9270): the gateway
|
||||||
# names (lowercase alpha, digits, dash, underscore). Names with
|
# nginx container lives in system config (not meta), so it can't
|
||||||
# other chars are an eval-time error rather than a silent wrong
|
# auto-rebuild from meta-flake events — the JSON file is what
|
||||||
# hash. Add entries here if hyperhive ever loosens the naming
|
# bridges the host's nix eval to the agent-lifecycle data c0re owns.
|
||||||
# constraint.
|
agentPortsTable =
|
||||||
charCode = {
|
if cfg.agentPortsFile == null || !builtins.pathExists cfg.agentPortsFile then
|
||||||
"0" = 48; "1" = 49; "2" = 50; "3" = 51; "4" = 52; "5" = 53;
|
{ }
|
||||||
"6" = 54; "7" = 55; "8" = 56; "9" = 57;
|
else
|
||||||
"-" = 45; "_" = 95;
|
builtins.fromJSON (builtins.readFile cfg.agentPortsFile);
|
||||||
"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).
|
||||||
|
|
@ -179,46 +143,39 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
agents = lib.mkOption {
|
agentPortsFile = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.nullOr lib.types.path;
|
||||||
default = [ ];
|
default = "/var/lib/hyperhive/agent-ports.json";
|
||||||
example = [
|
example = "/var/lib/hyperhive/agent-ports.json";
|
||||||
"iris"
|
|
||||||
"atlas"
|
|
||||||
"argus"
|
|
||||||
"damocles"
|
|
||||||
];
|
|
||||||
description = ''
|
description = ''
|
||||||
Sub-agent names to expose at `http(s)://<gateway>/agent/<name>/`
|
Path to a JSON file mapping sub-agent names to their web ports
|
||||||
through the gateway (#15 v0). For each name in the list, the
|
for `/agent/<name>/` routing through the gateway (#15 v0).
|
||||||
gateway adds a `location /agent/<name>/` block that
|
Shape: `{ "<name>": <port>, ... }`. Written by hive-c0re on
|
||||||
`proxy_pass`es to `http://127.0.0.1:<port>/`, where `<port>` is
|
every topology change (the rust side knows the canonical port
|
||||||
derived from the same `agent_web_port(name)` hash hive-c0re
|
allocation via `lifecycle::agent_web_port`; the gateway just
|
||||||
uses internally (`lifecycle.rs` constants + FNV-1a, replicated
|
reads what it's told).
|
||||||
in this module's `let` block).
|
|
||||||
|
|
||||||
**Purely additive** — the old `http://<host>:<port>/` direct
|
For each `<name>: <port>` entry, the gateway adds a
|
||||||
reach still works in parallel; this just gives the operator a
|
`location /agent/<name>/` block that `proxy_pass`es to
|
||||||
single-origin route. Manager isn't included (no per-agent
|
`http://127.0.0.1:<port>/`. Empty / missing file → no
|
||||||
prefix needed; manager already gets the `/` route via the
|
per-agent routes generated → gateway falls back to its pre-#15
|
||||||
c0re upstream block).
|
shape (just `/` + matrix surfaces).
|
||||||
|
|
||||||
Empty list (default) leaves the gateway in its pre-#15 shape:
|
**Purely additive**: the old `http://<host>:<port>/` direct
|
||||||
no per-agent routes, only `/` (c0re) + `/matrix/` (fluffychat
|
reach keeps working in parallel; this just gives the operator
|
||||||
when matrix is on) + `.well-known/matrix/*` (matrix
|
a single-origin route. Manager isn't included in the map (no
|
||||||
autodiscovery).
|
per-agent prefix needed; manager already gets the `/` route
|
||||||
|
via the c0re upstream block).
|
||||||
|
|
||||||
Maintenance: this list is currently operator-managed (the
|
Set to `null` to disable per-agent routing entirely without
|
||||||
gateway runs at the host level, the agent list lives in the
|
creating the file. Set to a custom path if the operator's c0re
|
||||||
meta-flake at runtime, and the host's nix eval doesn't see
|
writes the table elsewhere.
|
||||||
the meta-flake's agent dirs). Follow-up tracked at #15:
|
|
||||||
either auto-derive from
|
**Rebuild trigger**: the gateway container picks up new entries
|
||||||
`/var/lib/hyperhive/meta/topology.json` via `lib.importJSON`
|
on the next `nixos-rebuild switch` (or `hivectl gateway-sync`
|
||||||
in the gateway module (gives a single source of truth at the
|
if that helper lands). c0re writes are not auto-applied to a
|
||||||
cost of an eval-time impurity), or have c0re write an nginx
|
running gateway — see the follow-up in #15 for runtime nginx
|
||||||
snippet that the container `include`s + reloads on topology
|
include + reload + eventual per-agent unix sockets.
|
||||||
change (decouples from rebuilds entirely; also handles the
|
|
||||||
eventual move to per-agent unix sockets).
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -354,11 +311,10 @@ in
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
# Per-agent UIs (#15 v0). One `/agent/<name>/`
|
# Per-agent UIs (#15 v0). One `/agent/<name>/`
|
||||||
# block per name in `services.hyperhive.gateway.agents`,
|
# block per `<name>: <port>` entry in
|
||||||
# proxying to the agent's harness web server on
|
# `agentPortsTable` (loaded from `cfg.agentPortsFile`
|
||||||
# `127.0.0.1:<port>` where `<port>` comes from the
|
# — `/var/lib/hyperhive/agent-ports.json` by default,
|
||||||
# `agentWebPortLib` FNV-1a hash (matches
|
# written by hive-c0re on every topology change).
|
||||||
# `lifecycle::agent_web_port` line-for-line).
|
|
||||||
#
|
#
|
||||||
# Trailing-slash pair (`/agent/<name>/` + `proxy_pass
|
# Trailing-slash pair (`/agent/<name>/` + `proxy_pass
|
||||||
# http://...:<port>/`) strips the `/agent/<name>`
|
# http://...:<port>/`) strips the `/agent/<name>`
|
||||||
|
|
@ -378,23 +334,21 @@ in
|
||||||
# SSE / websocket support via `proxyWebsockets = true`
|
# SSE / websocket support via `proxyWebsockets = true`
|
||||||
# (same as the c0re `/` block below).
|
# (same as the c0re `/` block below).
|
||||||
#
|
#
|
||||||
# Empty `cfg.agents` list → empty attrset → no
|
# Empty / missing `cfg.agentPortsFile` → empty table
|
||||||
# per-agent blocks; old `<host>:<port>/` direct reach
|
# → no per-agent blocks; old `<host>:<port>/` direct
|
||||||
# still works.
|
# reach still works.
|
||||||
builtins.listToAttrs (
|
lib.mapAttrs' (name: port: {
|
||||||
builtins.map (name: {
|
name = "/agent/${name}/";
|
||||||
name = "/agent/${name}/";
|
value = {
|
||||||
value = {
|
proxyPass = "http://127.0.0.1:${toString port}/";
|
||||||
proxyPass = "http://127.0.0.1:${toString (agentWebPortLib name)}/";
|
proxyWebsockets = true;
|
||||||
proxyWebsockets = true;
|
extraConfig = ''
|
||||||
extraConfig = ''
|
proxy_set_header X-Forwarded-Prefix /agent/${name};
|
||||||
proxy_set_header X-Forwarded-Prefix /agent/${name};
|
proxy_buffering off;
|
||||||
proxy_buffering off;
|
proxy_read_timeout 1d;
|
||||||
proxy_read_timeout 1d;
|
'';
|
||||||
'';
|
};
|
||||||
};
|
}) agentPortsTable
|
||||||
}) 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