docs/gateway.md: extract big-picture gateway docs, trim in-code comments (mara #775:9988)
mara on PR #775: "this is too much docs in code - move bigger picture stuff to md files and put refs in code" New `docs/gateway.md` consolidates the gateway architecture story that was spreading across long inline comments in `hive-gateway.nix`, `hive-matrix.nix`, and `hive-forge.nix`: - vhost map (which URL serves what, which upstream, which option) - matrix discovery flow (.well-known → sub-domain delegation sequence) - Accept-header SPA fallback pattern (#686 / #729 design history) - local-dev `localHostsEntry` story - sub-domain rationale (mara verdict tracking) + when sub-path is right (hyperhive-internal apps) - per-vhost tuning knobs (forge LFS, matrix long-poll, agent SSE) - sequencing history (which PR added which routing piece) In-code comments in the two nix modules get trimmed to short refs into the doc — keeps the *why* in the markdown while the *what* stays alongside the code: - hive-gateway.nix: top-of-file comment, `agentPortsTable`, `appendHttpConfig`, every location block + vhost - hive-matrix.nix: `fluffychat-web-fixed`, `fluffychat-web-imaging`, the dart compile postInstall README.md gets a new row in the docs table pointing at gateway.md. Verified `nix eval` still resolves the same vhost + location layout after the comment trim — no behavioral change, just less in-code prose.
This commit is contained in:
parent
df549ed2a5
commit
922057e81b
4 changed files with 171 additions and 314 deletions
|
|
@ -10,25 +10,10 @@ let
|
|||
matrixCfg = config.services.hyperhive.matrix;
|
||||
forgeCfg = config.services.hyperhive.forge;
|
||||
|
||||
# Per-agent port table for `/agent/<name>/` 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 `{ "<name>": <port>, ... }`.
|
||||
#
|
||||
# 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.
|
||||
# Per-agent port table for `/agent/<name>/` routing. C0re writes
|
||||
# this JSON on every topology change; gateway reads at deploy time.
|
||||
# Missing file → empty map → no per-agent routes (graceful default).
|
||||
# See `docs/gateway.md` for the discovery + rebuild flow.
|
||||
agentPortsTable =
|
||||
if cfg.agentPortsFile == null || !builtins.pathExists cfg.agentPortsFile then
|
||||
{ }
|
||||
|
|
@ -36,21 +21,11 @@ let
|
|||
builtins.fromJSON (builtins.readFile cfg.agentPortsFile);
|
||||
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.
|
||||
# Single nginx in front of every hyperhive web surface — dashboard,
|
||||
# per-agent UIs (sub-path), forge + matrix (sub-domain), .well-known
|
||||
# delegations. Container `hive-gateway`, shared host netns,
|
||||
# state-free. Full vhost map + discovery flow + design rationale in
|
||||
# `docs/gateway.md`.
|
||||
|
||||
options.services.hyperhive.gateway = {
|
||||
enable = lib.mkOption {
|
||||
|
|
@ -209,32 +184,11 @@ in
|
|||
enable = true;
|
||||
recommendedProxySettings = true;
|
||||
recommendedOptimisation = true;
|
||||
# SPA-fallback target keyed on the `Accept` request header
|
||||
# (#686, mara + damocles on PR #729). This decides whether a
|
||||
# `/matrix/...` miss falls through to `index.html` (route
|
||||
# navigation) or returns a clean 404 (asset miss) — see the
|
||||
# `/matrix/` location comment below for the full rationale.
|
||||
#
|
||||
# Top-frame browser navigations always send
|
||||
# `Accept: text/html,...` (chrome/firefox/safari are
|
||||
# consistent on this). Asset fetches from script tags / img
|
||||
# / fetch() / XHR send asset-typed Accepts (`image/*`,
|
||||
# `application/javascript`, `*/*`) without `text/html`.
|
||||
# Mapping is purely on the header → no extension allowlist
|
||||
# to keep in sync with whatever the SPA ships, no regex
|
||||
# heuristic to false-positive on dot-segment routes.
|
||||
#
|
||||
# `$matrix_spa_target` defaults to a sentinel nonexistent
|
||||
# path so `try_files` falls through to the trailing `=404`
|
||||
# for asset misses. Browser navigations route to
|
||||
# `/matrix/index.html` where the SPA's client-side router
|
||||
# takes over.
|
||||
#
|
||||
# Only emitted when the matrix GUI is on (saves a no-op
|
||||
# `map` directive otherwise). Target now points at the
|
||||
# sub-domain-root `/index.html` (fluffychat moved off
|
||||
# `<hive>/matrix/` sub-path to `matrix.<hive>/` root in
|
||||
# #772; `--base-href` reverts to upstream-default `/`).
|
||||
# Accept-header SPA fallback (#686 / #729): navigations
|
||||
# (`Accept: text/html,...`) fall to index.html, asset
|
||||
# fetches (Accept *anything else*) fall to a sentinel
|
||||
# nonexistent path → `try_files` returns 404. Pattern
|
||||
# detailed in `docs/gateway.md` ("SPA fallback").
|
||||
appendHttpConfig = lib.optionalString (matrixCfg.enable && matrixCfg.gui.enable) ''
|
||||
map $http_accept $matrix_spa_target {
|
||||
default "/__matrix_spa_no_html_fallback";
|
||||
|
|
@ -250,19 +204,11 @@ in
|
|||
}
|
||||
];
|
||||
locations =
|
||||
# Bare-domain `<hive>/matrix/*` → 301 redirect to the
|
||||
# matrix sub-domain root (#772). fluffychat-web used to
|
||||
# live at this sub-path; #772 moved it to `matrix.<hive>/`
|
||||
# so it gets full sub-domain origin isolation + sub-spec
|
||||
# matches the matrix-spec deploy shape. The redirect
|
||||
# preserves bookmark + deep-link compatibility for
|
||||
# `<hive>/matrix/#/...` URLs during the transition;
|
||||
# operators can drop the redirect block once it's been
|
||||
# in the wild long enough that no stale bookmarks remain.
|
||||
#
|
||||
# Only emitted when both the matrix GUI is on AND
|
||||
# `matrixCfg.gatewayHost` is set (else there's no
|
||||
# canonical sub-domain to redirect to).
|
||||
# `<hive>/matrix/*` → 301 → `matrix.<hive>/$1`
|
||||
# (fluffychat moved to sub-domain root in #772; this
|
||||
# keeps bookmarks + deep-links working during the
|
||||
# transition). See `docs/gateway.md` for the vhost
|
||||
# map.
|
||||
lib.optionalAttrs (
|
||||
matrixCfg.enable
|
||||
&& matrixCfg.gui.enable
|
||||
|
|
@ -273,11 +219,6 @@ in
|
|||
target = "http://${matrixCfg.gatewayHost}${portSuffix}";
|
||||
in
|
||||
{
|
||||
# `rewrite ^/matrix/(.*)$ → matrix.<hive>/$1` —
|
||||
# strips the `/matrix/` prefix on the way out so
|
||||
# `<hive>/matrix/#/rooms/...` lands at the right
|
||||
# SPA route on the sub-domain side. `permanent`
|
||||
# emits 301 + sets the canonical Location header.
|
||||
"/matrix/" = {
|
||||
extraConfig = ''
|
||||
rewrite ^/matrix/(.*)$ ${target}/$1 permanent;
|
||||
|
|
@ -286,37 +227,14 @@ in
|
|||
}
|
||||
)
|
||||
//
|
||||
# `.well-known/matrix/*` auto-discovery (#660): when
|
||||
# `services.hyperhive.matrix.enable` is on and the
|
||||
# operator's set a hive domain, the gateway serves
|
||||
# the matrix-spec discovery JSON at the canonical
|
||||
# location so clients pointed at `${hyperhive.domain}`
|
||||
# resolve through to the actual tuwunel endpoint
|
||||
# without needing a `matrix.` subdomain.
|
||||
#
|
||||
# `m.homeserver.base_url` advertises the client-server
|
||||
# API. `m.server` advertises the federation
|
||||
# `host:port` (tuwunel serves both client + federation
|
||||
# on the same `httpPort` — see hive-matrix.nix).
|
||||
#
|
||||
# CORS `*` on the client endpoint per the matrix spec
|
||||
# (https://spec.matrix.org/v1.15/client-server-api/#getwell-knownmatrixclient).
|
||||
# No-op until the operator turns matrix on; until then
|
||||
# there's no homeserver to advertise.
|
||||
# `.well-known/matrix/{client,server}` discovery JSON.
|
||||
# Points clients at `matrixCfg.gatewayHost` (sub-domain
|
||||
# vhost) when set; falls back to direct `<hive>:<httpPort>`
|
||||
# when no gateway target. CORS `*` per matrix spec.
|
||||
# See `docs/gateway.md` "Discovery flow" for the full
|
||||
# client-bootstrap sequence.
|
||||
lib.optionalAttrs (matrixCfg.enable && hyperhiveDomain != null) (
|
||||
let
|
||||
# `.well-known/matrix/{client,server}` advertise where
|
||||
# the actual matrix API lives. When `matrixCfg.gatewayHost`
|
||||
# is set (default `matrix.<hive-domain>`, #747), point
|
||||
# at the sub-domain — no port suffix when the gateway
|
||||
# is on the canonical port 80, transparent to clients
|
||||
# (mara on #749:9609 sub-domain verdict, "not user-
|
||||
# visible because the .well-known redirect routes
|
||||
# clients through automatically"). When `gatewayHost`
|
||||
# is unset (no hive-domain, or operator nulled it),
|
||||
# fall back to the direct `host:port` shape — clients
|
||||
# reach tuwunel without going through the gateway,
|
||||
# no sub-domain delegation.
|
||||
portSuffix = if cfg.port == 80 then "" else ":${toString cfg.port}";
|
||||
clientBaseUrl =
|
||||
if matrixCfg.gatewayHost != null then
|
||||
|
|
@ -346,33 +264,12 @@ in
|
|||
}
|
||||
)
|
||||
//
|
||||
# Per-agent UIs (#15 v0). One `/agent/<name>/`
|
||||
# block per `<name>: <port>` 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/<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 / missing `cfg.agentPortsFile` → empty table
|
||||
# → no per-agent blocks; old `<host>:<port>/` direct
|
||||
# reach still works.
|
||||
# Per-agent UIs (#15 v0). One `/agent/<name>/` block
|
||||
# per entry in `agentPortsTable`. Trailing-slash pair
|
||||
# strips the prefix; `X-Forwarded-Prefix` lets the
|
||||
# harness build absolute URLs when relative isn't
|
||||
# enough. See `docs/gateway.md` for the vhost map
|
||||
# + tuning rationale.
|
||||
lib.mapAttrs' (name: port: {
|
||||
name = "/agent/${name}/";
|
||||
value = {
|
||||
|
|
@ -402,31 +299,11 @@ in
|
|||
};
|
||||
}
|
||||
//
|
||||
# Forge vhost (#749, mara verdict at issue:9609 —
|
||||
# sub-domain over sub-path). When forgejo runs behind the
|
||||
# gateway (`forge.behindGateway = true`), it gets its own
|
||||
# `server { server_name = forge.domain; }` block. The
|
||||
# block proxies all `/` → `http://127.0.0.1:<forge.httpPort>/`
|
||||
# so forgejo handles requests at root (default deploy shape
|
||||
# — no `ROOT_URL`-prefix translation needed).
|
||||
#
|
||||
# `forge.domain` is the full hostname (e.g.
|
||||
# `forge.darkest.space`, `git.example.com`) — single source
|
||||
# of truth for both the forgejo `DOMAIN` setting and the
|
||||
# gateway vhost name (mara on #754:9684 — "specify full
|
||||
# forge domain in options instead").
|
||||
#
|
||||
# `client_max_body_size 1G` — git pushes + LFS uploads can
|
||||
# be large; nginx's default 1M would 413 most real commits.
|
||||
#
|
||||
# Long timeouts for big repo operations: a fresh clone of a
|
||||
# multi-GB repo can take minutes; the default 60s
|
||||
# `proxy_read_timeout` would abort mid-stream.
|
||||
#
|
||||
# `proxyWebsockets = true` keeps forgejo's live-update
|
||||
# endpoints (`/api/v1/events`) + any future websocket
|
||||
# endpoints working transparently. SSH stays direct on
|
||||
# `forge.sshPort` (separate listener protocol, not HTTP).
|
||||
# Forge sub-domain vhost (#749 / #754). `server_name =
|
||||
# forge.domain`, proxies all `/` → forgejo. Tuned for
|
||||
# git: `client_max_body_size 1G`, `proxy_read_timeout 1h`
|
||||
# (multi-GB clones). SSH stays direct on `forge.sshPort`.
|
||||
# See `docs/gateway.md`.
|
||||
lib.optionalAttrs (forgeCfg.enable or false && forgeCfg.behindGateway or false) {
|
||||
"${forgeCfg.domain}" = {
|
||||
listen = [
|
||||
|
|
@ -448,34 +325,12 @@ in
|
|||
};
|
||||
}
|
||||
//
|
||||
# Matrix homeserver vhost (#747, mara verdict on #749:9609 —
|
||||
# sub-domain over sub-path for matrix; "not user-visible"
|
||||
# because clients discover the sub-domain via the
|
||||
# `.well-known/matrix/{client,server}` delegation served
|
||||
# above on the bare hive-domain).
|
||||
#
|
||||
# `server { server_name = matrixCfg.gatewayHost; }` proxies
|
||||
# `/_matrix/...` → `http://127.0.0.1:''${matrixCfg.httpPort}/_matrix/...`.
|
||||
# Tuwunel listens on `:''${httpPort}` (default 8008); the
|
||||
# gateway terminates on `:''${cfg.port}` (80) so external
|
||||
# clients speak matrix over the canonical web port without
|
||||
# operators having to open the tuwunel port through firewalls.
|
||||
#
|
||||
# `/` returns 404 — nothing else lives at the matrix vhost;
|
||||
# the matrix client-server API is entirely under `/_matrix/`,
|
||||
# and federation under `/_matrix/federation/...`.
|
||||
#
|
||||
# CORS `*` on the matrix vhost per the matrix spec —
|
||||
# federation + client requests come from any origin.
|
||||
#
|
||||
# `client_max_body_size 50M` covers typical media uploads
|
||||
# (matrix-spec media size cap default); operators with bigger
|
||||
# uploads override via the matrix module's own cap when that
|
||||
# lands.
|
||||
#
|
||||
# `proxy_read_timeout 1h` for long-poll `/sync`; the default
|
||||
# 60s would abort `/sync?timeout=30000` legitimately when
|
||||
# tuwunel's keepalive exceeds that.
|
||||
# Matrix sub-domain vhost (#747 / #764). `server_name =
|
||||
# matrixCfg.gatewayHost`. `/_matrix/*` → tuwunel (CORS *,
|
||||
# 50M body cap, 1h long-poll timeout). `/` serves
|
||||
# fluffychat (#772) or 404 if GUI off. nginx
|
||||
# longer-prefix-wins puts `/_matrix/` ahead of `/`.
|
||||
# See `docs/gateway.md`.
|
||||
lib.optionalAttrs (matrixCfg.enable && matrixCfg.gatewayHost != null) {
|
||||
"${matrixCfg.gatewayHost}" = {
|
||||
listen = [
|
||||
|
|
@ -498,49 +353,34 @@ in
|
|||
'';
|
||||
};
|
||||
}
|
||||
//
|
||||
# Fluffychat-web (or override) served at sub-domain
|
||||
# root (#772 — moved from bare-domain `/matrix/`).
|
||||
# nginx location-precedence: longer prefix wins,
|
||||
# so `/_matrix/` (above) handles the matrix API
|
||||
# and `/` falls through to fluffychat for
|
||||
# everything else.
|
||||
#
|
||||
# SPA fallback uses the same Accept-header `$matrix_spa_target`
|
||||
# map from `appendHttpConfig` above — navigations
|
||||
# fall to `/index.html` (now sub-domain-root path),
|
||||
# asset misses return clean 404.
|
||||
#
|
||||
# `= /config.json` serves the FluffyChat boot-config
|
||||
# pre-fill with the operator's hive-domain so the
|
||||
# client's `.well-known/matrix/client` lookup hits
|
||||
# the right delegation endpoint (#736).
|
||||
lib.optionalAttrs (matrixCfg.gui.enable) (
|
||||
{
|
||||
"/" = {
|
||||
alias = "${matrixCfg.gui.package}/";
|
||||
extraConfig = ''
|
||||
try_files $uri $uri/ $matrix_spa_target =404;
|
||||
'';
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs (hyperhiveDomain != null) {
|
||||
"= /config.json" = {
|
||||
extraConfig = ''
|
||||
default_type application/json;
|
||||
return 200 '{"defaultHomeserver":"${hyperhiveDomain}"}';
|
||||
'';
|
||||
};
|
||||
}
|
||||
)
|
||||
//
|
||||
# Fall-through `/` when GUI is off: nothing else
|
||||
# lives at the matrix vhost, return 404 cleanly.
|
||||
lib.optionalAttrs (!matrixCfg.gui.enable) {
|
||||
// lib.optionalAttrs (matrixCfg.gui.enable) (
|
||||
{
|
||||
# fluffychat at sub-domain root, SPA-fallback via
|
||||
# the Accept-header `$matrix_spa_target` map.
|
||||
"/" = {
|
||||
return = "404";
|
||||
alias = "${matrixCfg.gui.package}/";
|
||||
extraConfig = ''
|
||||
try_files $uri $uri/ $matrix_spa_target =404;
|
||||
'';
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs (hyperhiveDomain != null) {
|
||||
# FluffyChat boot-config pre-fill so the client's
|
||||
# `.well-known/matrix/client` lookup hits the
|
||||
# right delegation endpoint (#736).
|
||||
"= /config.json" = {
|
||||
extraConfig = ''
|
||||
default_type application/json;
|
||||
return 200 '{"defaultHomeserver":"${hyperhiveDomain}"}';
|
||||
'';
|
||||
};
|
||||
}
|
||||
)
|
||||
// lib.optionalAttrs (!matrixCfg.gui.enable) {
|
||||
"/" = {
|
||||
return = "404";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -551,17 +391,10 @@ in
|
|||
allowedTCPPorts = [ cfg.port ];
|
||||
};
|
||||
|
||||
# `/etc/hosts` entries for local dev: the bare hive domain plus
|
||||
# any sub-domain modules (forge via #749/#754, matrix via #747)
|
||||
# that are on. All map to `127.0.0.1` since the gateway shares
|
||||
# host netns. Operators with real DNS leave `localHostsEntry =
|
||||
# false`; this is the dev-loop shortcut for `http://<hive-domain>/`
|
||||
# + `http://forge.<hive-domain>/` + `http://matrix.<hive-domain>/`
|
||||
# resolving locally.
|
||||
#
|
||||
# `lib.unique` collapses any duplicate (e.g. if forge.domain
|
||||
# happens to equal hyperhiveDomain or matrixCfg.gatewayHost) so
|
||||
# `/etc/hosts` doesn't carry the same entry twice.
|
||||
# `/etc/hosts` entries for local dev — bare hive domain + any
|
||||
# sub-domain modules that are on. `lib.unique` dedupes if any
|
||||
# sub-domain happens to equal another. See `docs/gateway.md`
|
||||
# ("Local dev").
|
||||
networking.hosts = lib.mkIf (cfg.localHostsEntry && hyperhiveDomain != null) {
|
||||
"127.0.0.1" = lib.unique (
|
||||
[ hyperhiveDomain ]
|
||||
|
|
|
|||
Loading…
Reference in a new issue