gateway: hot-reload agents.conf at runtime (#869)

Replace eval-time per-agent nginx location baking with a runtime
include file. c0re writes /var/lib/hyperhive/agents.conf (nginx
location blocks, UDS or TCP per agent) on every topology change and
on the 10s marker poll. The gateway container bind-mounts
/var/lib/hyperhive/ at /run/hive-state/ and nginx includes
/run/hive-state/agents.conf. A systemd path unit inside the container
watches the file for changes and fires `nginx -s reload` on each
atomic rename from c0re — no nixos-rebuild switch needed when agents
start, stop, or flip useUnixSocket.

  - new hive-c0re/src/gateway_nginx.rs: write() + render()
  - lib.rs + meta.rs + agent_sockets::spawn_poll: hook in write()
  - hive-gateway.nix: drop agentPortsTable/agentSocketsTable/
    agentUpstreamFor/lib.mapAttrs', add /run/hive-state bind-mount,
    include directive, systemd path unit + reload service, tmpfiles
    for /var/lib/hyperhive + agents.conf seed
  - docs/gateway.md: update vhost table + Per-agent UDS section
This commit is contained in:
atlas 2026-05-31 20:12:00 +02:00 committed by mara
commit 07434e8f50
7 changed files with 337 additions and 173 deletions

View file

@ -7,7 +7,7 @@ Single nginx in front of every hyperhive web surface. Container `hive-gateway`,
| URL | vhost | upstream | source |
| --- | --- | --- | --- |
| `<hive>/` | `_` (catch-all) | hive-c0re dashboard (`7000`) | always |
| `<hive>/agent/<name>/` | `_` | per-agent harness on `agent_web_port(name)` | `agentPortsFile` JSON |
| `<hive>/agent/<name>/` | `_` | per-agent harness (UDS or TCP) | `agents.conf` (runtime-generated) |
| `<hive>/.well-known/matrix/{client,server}` | `_` | inline JSON (no upstream) | `matrix.enable && domain != null` |
| `<hive>/matrix/` (deprecated) | `_` | 301 → `matrix.<hive>/` | `matrix.gui.enable` |
| `forge.<hive>/` | `forge.<hive>` | forgejo (`3000`) | `forge.behindGateway` |
@ -89,21 +89,27 @@ unix-domain socket as each agent opts in. The mechanism:
harness has actually bound the socket appear there. Without this
filter, the gateway would `proxy_pass` to a non-existent socket
for every sub-agent that hasn't opted in yet.
4. **Gateway side**. Reads `agent-sockets.json` at request-handling
time and routes `/agent/<name>/` to
`http://unix:/run/hive-agent/<name>/web.sock:/`. Whole
`/run/hive-agent/` is bind-mounted read-only into the gateway
container so it can reach every published socket.
4. **Gateway side**. `gateway_nginx::write` generates
`/var/lib/hyperhive/agents.conf` — a plain nginx include file with
one `location /agent/<name>/` block per agent. UDS upstream
(`http://unix:/run/hive-agent/<name>/web.sock:/`) when `.bound`
marker present; TCP loopback otherwise. The gateway container
bind-mounts `/var/lib/hyperhive/` at `/run/hive-state/`; nginx
includes `/run/hive-state/agents.conf`. A systemd path unit
(`hive-gateway-agents-conf.path`) inside the container watches the
file and fires `nginx -s reload` on every atomic rename from c0re
— no `nixos-rebuild` needed (#869).
c0re re-fires `agent_sockets::write` every 10s so newly-bound
markers get picked up without needing a container-start hook in
every lifecycle path. `write()` is idempotent: steady-state cost is
one stat per agent per tick.
c0re regenerates `agents.conf` (and fires the path unit → reload) on
two triggers: every topology change (new/removed agents) and every
10s marker poll tick (`agent_sockets::spawn_poll`). `write()` is
idempotent — skips the rename when content is unchanged so the path
unit doesn't fire spuriously.
Transition: agents that haven't flipped `useUnixSocket = true` still
appear in `agent-ports.json` (the legacy TCP map) and the gateway
falls back to TCP for them. A future cleanup will drop the TCP map +
the harness's TCP bind once every agent's flipped.
Transition: agents that haven't flipped `useUnixSocket = true` get a
TCP loopback upstream in `agents.conf` (deterministic port from
`agent_web_port(name)`). A future cleanup will drop the TCP fallback
once every agent's flipped.
## Dashboard link shape (gateway vs direct)