Compare commits

...
Author SHA1 Message Date
atlas
8d25b087b6 nix/hive-matrix: trim option-description prose to docs/gateway.md refs (argus #775 🔴)
argus on #775 v3: "the `gatewayHost` option description's
server_name-vs-gatewayHost essay + federation SRV note are also
candidates for [docs/gateway.md] section."

Cuts the gatewayHost option's description from ~40 lines (with
inline duplication of the discovery flow, when-to-set-which, and
federation port caveat) down to ~8 lines pointing at
`docs/gateway.md`. The brief `server_name vs gatewayHost` clarifier
stays in code because it disambiguates two SIMILAR-LOOKING options
on the same module — operators reading option docs need the
distinction inline, not behind a doc link.

Also trimmed `matrix.gui.enable` + `matrix.gui.package` descriptions
to similar shapes — point at docs/gateway.md for the architecture,
keep the override-shape hints in code.

Push includes the rebase onto current main (#764 + 0af6ea1 + others
landed since #775 was opened; cherry-picked commits get skipped
cleanly).

Net: matrix.nix loses ~70 lines of inline prose. No behavioral
change (verified gatewayHost still resolves to `matrix.<hive>`).
2026-05-31 14:29:40 +02:00
atlas
922057e81b 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.
2026-05-31 14:29:40 +02:00
atlas
df549ed2a5 nix/hive-{matrix,gateway}: host fluffychat at matrix.<hive>/, 301 from <hive>/matrix/ (#772)
mara on #764:9897: "host the fluffy chat app at / as follow up?"

Moves fluffychat-web from the bare-domain sub-path
(`<hive>/matrix/`) to the matrix sub-domain root
(`matrix.<hive>/`). Follow-up to #764 (matrix vhost itself), per
mara's gateway-architecture verdict (sub-domain for external standard
apps, sub-path for hyperhive-internal). Stacked on
`atlas/747-matrix-behind-gateway` — depends on #764 landing first.

## Mechanics

**hive-matrix.nix:**
- Drop `flutterBuildFlags = [ "--base-href" "/matrix/" ]` from
  `fluffychat-web-fixed`. Upstream default `--base-href "/"` is correct
  at sub-domain root.
- Update option docs to reflect new mount point.

**hive-gateway.nix:**
- `$matrix_spa_target` map target flips from `/matrix/index.html` →
  `/index.html` (sub-domain root now).
- New `<hive>/matrix/*` location: `rewrite ^/matrix/(.*)$
  matrix.<hive>/$1 permanent;` — 301 redirect preserves bookmark +
  deep-link compatibility for `<hive>/matrix/#/rooms/...` URLs during
  the transition.
- `<hive>/matrix/config.json` location removed (moved to `/config.json`
  on the matrix vhost).
- Matrix vhost (#764) gains `/` location: serves fluffychat dist as
  static files with the Accept-header SPA fallback (`/_matrix/`
  proxying to tuwunel keeps working via nginx longer-prefix-wins
  precedence). When `gui.enable = false`, `/` returns 404 cleanly.
- Matrix vhost gains `= /config.json` for the FluffyChat boot-config
  pre-fill (#736).

## Verified

```
vhosts:        ["_", "forge.test.local", "matrix.test.local"]
bare locations: ["/", "/matrix/", "= /.well-known/matrix/client",
                 "= /.well-known/matrix/server"]
matrix vhost locations: ["/", "/_matrix/", "= /config.json"]
/matrix/ extraConfig: "rewrite ^/matrix/(.*)$ http://matrix.test.local/$1 permanent;"
matrix vhost / alias: /nix/store/...fluffychat-web-2.6.0/
```

Full container toplevel builds clean.

## Risk

Medium. Two breaking changes for operators:

1. **Bookmark migration**: `http://<hive>/matrix/#/rooms/...` 301s
   to `http://matrix.<hive>/#/rooms/...`. Browser bookmarks +
   shared links keep working via the redirect; can be cleaned up
   once it's been in the wild long enough.

2. **fluffychat-web dist hash changes**: dropping the
   `--base-href "/matrix/"` flag changes the derivation hash, so
   `gui.package` rebuilds even though the source is the same.
   Operators on substitute caches will fetch the new dist; building
   from source takes the same time as before.

The `.well-known/matrix/{client,server}` delegation (already
advertising `matrix.<hive>` per #764) means matrix clients
auto-discover the new location — no client config change needed.

## Sequencing

**Depends on #764** — needs the matrix vhost to host the new `/`
location. Merge after #764 lands + soaks. If #764 changes shape
during review I'll rebase + force-push.

Closes #772.
2026-05-31 14:29:40 +02:00
4 changed files with 229 additions and 408 deletions

View file

@ -42,6 +42,7 @@ Depth lives in [`docs/`](docs/) — pick the one matching your task:
| config-edit + approval state machine | [`docs/approvals.md`](docs/approvals.md) | | config-edit + approval state machine | [`docs/approvals.md`](docs/approvals.md) |
| what survives destroy / purge / restart | [`docs/persistence.md`](docs/persistence.md) | | what survives destroy / purge / restart | [`docs/persistence.md`](docs/persistence.md) |
| naming, wire protocol, commit style | [`docs/conventions.md`](docs/conventions.md) | | naming, wire protocol, commit style | [`docs/conventions.md`](docs/conventions.md) |
| nginx vhost map + sub-domain routing | [`docs/gateway.md`](docs/gateway.md) |
| NixOS / nspawn gotchas | [`docs/gotchas.md`](docs/gotchas.md) | | NixOS / nspawn gotchas | [`docs/gotchas.md`](docs/gotchas.md) |
## Host config ## Host config

78
docs/gateway.md Normal file
View file

@ -0,0 +1,78 @@
# hive-gateway
Single nginx in front of every hyperhive web surface. Container `hive-gateway`, shared host netns, system-config (not meta-flake managed). Configured via `services.hyperhive.gateway.*` + per-subsystem opt-in flags in `services.hyperhive.{forge,matrix,...}`.
## Vhost map
| 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, #15 |
| `<hive>/.well-known/matrix/{client,server}` | `_` | inline JSON (no upstream) | `matrix.enable && domain != null`, #660 / #747 |
| `<hive>/matrix/` (deprecated) | `_` | 301 → `matrix.<hive>/` | `matrix.gui.enable`, #772 |
| `forge.<hive>/` | `forge.<hive>` | forgejo (`3000`) | `forge.behindGateway`, #754 |
| `matrix.<hive>/_matrix/*` | `matrix.<hive>` | tuwunel (`8008`) | `matrix.gatewayHost != null`, #764 |
| `matrix.<hive>/` | `matrix.<hive>` | fluffychat-web static | `matrix.gui.enable`, #772 |
| `matrix.<hive>/config.json` | `matrix.<hive>` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null`, #736 |
Per-agent UIs stay sub-path because they're hyperhive-internal and base-path-aware (iris #731). External standard apps (forge / matrix) get sub-domains because their defaults work cleanly at sub-domain root + per-origin cookies / storage isolation matters.
## Discovery flow (matrix)
Operator points client at `<hive>`. Sequence:
1. Client fetches `http://<hive>/.well-known/matrix/client``{"m.homeserver":{"base_url":"http://matrix.<hive>"}}` (no port suffix when gateway listens on 80).
2. Client connects to `matrix.<hive>/_matrix/client/...`.
3. Gateway routes `/_matrix/*` → tuwunel at `127.0.0.1:8008`.
Federation peers fetch `.well-known/matrix/server``{"m.server":"matrix.<hive>"}` and connect to `matrix.<hive>:8448` per spec default. Gateway only listens on configured `port`; cross-hive federation needs either an SRV record (`_matrix._tcp.matrix.<hive>` → port 80) OR `matrix.openFirewall = true` so peers reach tuwunel's federation port directly. Hyperhive is mostly closed/internal, so this rarely bites.
## SPA fallback (Accept-header pattern)
The `<hive>` catch-all and the `matrix.<hive>` vhost both serve a flutter SPA (per-agent UI, fluffychat). Two requirements collide:
- hard-refresh on a sub-route must serve `index.html` (SPA's client-side router takes over after JS bootstrap)
- missing assets must surface as 404, not as HTML with wrong content-type (the original #643 bug)
Solution: an `nginx http`-context `map $http_accept $matrix_spa_target { ... }` keyed on the request's Accept header. Browser navigations (`Accept: text/html,...`) get `index.html`; asset fetches (`Accept: image/*`, `*/*`, etc.) get a sentinel nonexistent path → `try_files` falls through to `=404`. No extension allowlist, no `if` block, no regex heuristics. #686 + #729 thread for the design history.
## Local dev (`localHostsEntry`)
`services.hyperhive.gateway.localHostsEntry = true` adds entries to the host's `/etc/hosts`:
- `<hive-domain>``127.0.0.1`
- `forge.<hive>``127.0.0.1` (when forge.behindGateway)
- `matrix.<hive>``127.0.0.1` (when matrix.gatewayHost set)
`lib.unique` de-dupes if any sub-domain happens to equal another entry. Operators with real DNS leave it off.
## Sub-domain shape (rationale)
mara verdict at #749:9609 + #747:9722: sub-domain over sub-path for forge + matrix, sub-path for per-agent UIs.
- forgejo's default `ROOT_URL = http://<host>/` works without any `X-Forwarded-Prefix` gymnastics — sub-domain hosting is the canonical Forgejo deploy shape.
- matrix-spec deployments universally use `matrix.<server_name>` for the actual API listener — federation already expects this.
- per-agent UIs are hyperhive-internal; iris's #731 made them base-path-aware specifically for `/agent/<name>/`. Sub-domain per agent would multiply DNS + TLS-per-subdomain cost without per-app config wins.
- cookie / storage isolation: a future forge XSS can't reach the dashboard session because they're different origins.
`services.hyperhive.{forge.domain,matrix.gatewayHost}` take the full hostname (`forge.darkest.space`, `git.example.com`) rather than a label that gets concatenated with hive-domain — mara on #754:9684 wanted operator control over the full shape, not a forced `<label>.<hive-domain>` pattern.
## Tuning knobs
Per-vhost timeouts + body-size limits live in the location blocks:
- forge `/` (forgejo): `client_max_body_size 1G` (LFS), `proxy_read_timeout 1h` (multi-GB clones), `proxyWebsockets = true` (live-update endpoints).
- matrix `/_matrix/` (tuwunel): `client_max_body_size 50M` (media uploads), `proxy_read_timeout 1h` (long-poll `/sync`), CORS `*` (federation + cross-origin clients), `proxyWebsockets = true`.
- per-agent `/agent/<name>/`: `proxy_read_timeout 1d` (long-lived SSE / WebSocket dashboards), `proxyWebsockets = true`, `X-Forwarded-Prefix` set so the harness can build absolute URLs when relative isn't enough.
SSH for forge stays direct on `cfg.sshPort` — separate listener protocol, not HTTP-over-nginx.
## Sequencing history
- #15 v0 (per-agent routing, #740) — first sub-app behind the gateway, JSON port table from c0re.
- #686 / #729 — Accept-header SPA fallback pattern.
- #749 / #754 — forge to sub-domain (mara: sub-domain over sub-path).
- #747 / #764 — matrix sub-domain vhost + `.well-known` delegation.
- #772 / #775 — fluffychat hops from `<hive>/matrix/` to `matrix.<hive>/`.
Next-up tracked separately: #14 (container netns isolation), TLS (#594).

View file

@ -10,25 +10,10 @@ let
matrixCfg = config.services.hyperhive.matrix; matrixCfg = config.services.hyperhive.matrix;
forgeCfg = config.services.hyperhive.forge; forgeCfg = config.services.hyperhive.forge;
# Per-agent port table for `/agent/<name>/` routing (#15 v0). Single- # Per-agent port table for `/agent/<name>/` routing. C0re writes
# sourced from `cfg.agentPortsFile` (default # this JSON on every topology change; gateway reads at deploy time.
# `/var/lib/hyperhive/agent-ports.json`), written by hive-c0re on every # Missing file → empty map → no per-agent routes (graceful default).
# topology change in shape `{ "<name>": <port>, ... }`. # See `docs/gateway.md` for the discovery + rebuild flow.
#
# 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 = agentPortsTable =
if cfg.agentPortsFile == null || !builtins.pathExists cfg.agentPortsFile then if cfg.agentPortsFile == null || !builtins.pathExists cfg.agentPortsFile then
{ } { }
@ -36,21 +21,11 @@ let
builtins.fromJSON (builtins.readFile cfg.agentPortsFile); builtins.fromJSON (builtins.readFile cfg.agentPortsFile);
in in
{ {
# Single nginx in front of every hyperhive surface (#609 / #15 v0). # Single nginx in front of every hyperhive web surface — dashboard,
# Lives in its own nixos-container (like hive-forge / hive-matrix) so # per-agent UIs (sub-path), forge + matrix (sub-domain), .well-known
# the operator can opt out without touching the host's own nginx, and # delegations. Container `hive-gateway`, shared host netns,
# so the static-serve responsibility for the matrix GUI moves off # state-free. Full vhost map + discovery flow + design rationale in
# hive-c0re's axum router. Shares host netns so `localhost` # `docs/gateway.md`.
# 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.
options.services.hyperhive.gateway = { options.services.hyperhive.gateway = {
enable = lib.mkOption { enable = lib.mkOption {
@ -209,33 +184,15 @@ in
enable = true; enable = true;
recommendedProxySettings = true; recommendedProxySettings = true;
recommendedOptimisation = true; recommendedOptimisation = true;
# SPA-fallback target keyed on the `Accept` request header # Accept-header SPA fallback (#686 / #729): navigations
# (#686, mara + damocles on PR #729). This decides whether a # (`Accept: text/html,...`) fall to index.html, asset
# `/matrix/...` miss falls through to `index.html` (route # fetches (Accept *anything else*) fall to a sentinel
# navigation) or returns a clean 404 (asset miss) — see the # nonexistent path → `try_files` returns 404. Pattern
# `/matrix/` location comment below for the full rationale. # detailed in `docs/gateway.md` ("SPA fallback").
#
# 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).
appendHttpConfig = lib.optionalString (matrixCfg.enable && matrixCfg.gui.enable) '' appendHttpConfig = lib.optionalString (matrixCfg.enable && matrixCfg.gui.enable) ''
map $http_accept $matrix_spa_target { map $http_accept $matrix_spa_target {
default "/__matrix_spa_no_html_fallback"; default "/__matrix_spa_no_html_fallback";
"~*text/html" "/matrix/index.html"; "~*text/html" "/index.html";
} }
''; '';
virtualHosts = { virtualHosts = {
@ -247,99 +204,37 @@ in
} }
]; ];
locations = locations =
# Matrix GUI: when the operator has flipped both # `<hive>/matrix/*` → 301 → `matrix.<hive>/$1`
# `services.hyperhive.matrix.enable` and `matrix.gui.enable` # (fluffychat moved to sub-domain root in #772; this
# on, nginx serves fluffychat-web (or whatever override) # keeps bookmarks + deep-links working during the
# as a static dist at `/matrix/`. # transition). See `docs/gateway.md` for the vhost
# # map.
# SPA fallback (iris/#643, rewritten in #686 per mara lib.optionalAttrs (
# + damocles on PR #729): the original matrixCfg.enable
# `try_files $uri $uri/ /matrix/index.html;` shape && matrixCfg.gui.enable
# silently masked missing assets — flutter's bootstrap && matrixCfg.gatewayHost != null
# requesting e.g. `/matrix/native_executor.js` got ) (
# `index.html` (Content-Type: text/html, status 200) let
# when the file was absent from the dist, so the JS portSuffix = if cfg.port == 80 then "" else ":${toString cfg.port}";
# runtime never loaded and `/matrix/` rendered blank target = "http://${matrixCfg.gatewayHost}${portSuffix}";
# without any visible error. in
# {
# The followup #729 narrowed it with an extension "/matrix/" = {
# allowlist; this version uses `$matrix_spa_target` extraConfig = ''
# (defined in the `appendHttpConfig` above, keyed on rewrite ^/matrix/(.*)$ ${target}/$1 permanent;
# the `Accept` header) so the decision lives in HTTP '';
# semantics rather than a maintained extension list. };
# Navigations (Accept: text/html) fall to index.html; }
# asset fetches (Accept: */*, image/*, etc.) get a )
# clean 404 via the trailing `=404`.
lib.optionalAttrs (matrixCfg.enable && matrixCfg.gui.enable) {
"/matrix/" = {
alias = "${matrixCfg.gui.package}/";
extraConfig = ''
try_files $uri $uri/ $matrix_spa_target =404;
'';
};
# FluffyChat fetches `/matrix/config.json` directly on
# boot for its own branding + default-homeserver
# bootstrap, BEFORE asking the user to pick a server
# (#736). The upstream `pkgs.fluffychat-web` dist
# ships without one, so the fetch 404s and the user
# sees the empty "enter homeserver" prompt. Serve a
# minimal config that pre-fills `defaultHomeserver`
# with the operator's hive domain — the matrix
# client then runs `.well-known/matrix/client` against
# that domain (already served by the
# `= /.well-known/matrix/client` block below) and
# discovers the actual tuwunel endpoint.
#
# Only the `defaultHomeserver` field is overridden —
# everything else (branding, audio defaults, etc.)
# falls back to fluffychat's hardcoded defaults so
# we don't pin against upstream config-schema drift.
# No-op when `services.hyperhive.domain` is unset
# (the location block is omitted entirely; the SPA
# then falls back to its empty form, same as before
# #736).
} // lib.optionalAttrs (
matrixCfg.enable && matrixCfg.gui.enable && hyperhiveDomain != null
) {
"= /matrix/config.json" = {
extraConfig = ''
default_type application/json;
return 200 '{"defaultHomeserver":"${hyperhiveDomain}"}';
'';
};
}
// //
# `.well-known/matrix/*` auto-discovery (#660): when # `.well-known/matrix/{client,server}` discovery JSON.
# `services.hyperhive.matrix.enable` is on and the # Points clients at `matrixCfg.gatewayHost` (sub-domain
# operator's set a hive domain, the gateway serves # vhost) when set; falls back to direct `<hive>:<httpPort>`
# the matrix-spec discovery JSON at the canonical # when no gateway target. CORS `*` per matrix spec.
# location so clients pointed at `${hyperhive.domain}` # See `docs/gateway.md` "Discovery flow" for the full
# resolve through to the actual tuwunel endpoint # client-bootstrap sequence.
# 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.
lib.optionalAttrs (matrixCfg.enable && hyperhiveDomain != null) ( lib.optionalAttrs (matrixCfg.enable && hyperhiveDomain != null) (
let 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}"; portSuffix = if cfg.port == 80 then "" else ":${toString cfg.port}";
clientBaseUrl = clientBaseUrl =
if matrixCfg.gatewayHost != null then if matrixCfg.gatewayHost != null then
@ -369,33 +264,12 @@ in
} }
) )
// //
# Per-agent UIs (#15 v0). One `/agent/<name>/` # Per-agent UIs (#15 v0). One `/agent/<name>/` block
# block per `<name>: <port>` entry in # per entry in `agentPortsTable`. Trailing-slash pair
# `agentPortsTable` (loaded from `cfg.agentPortsFile` # strips the prefix; `X-Forwarded-Prefix` lets the
# — `/var/lib/hyperhive/agent-ports.json` by default, # harness build absolute URLs when relative isn't
# written by hive-c0re on every topology change). # enough. See `docs/gateway.md` for the vhost map
# # + tuning rationale.
# 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.
lib.mapAttrs' (name: port: { lib.mapAttrs' (name: port: {
name = "/agent/${name}/"; name = "/agent/${name}/";
value = { value = {
@ -425,31 +299,11 @@ in
}; };
} }
// //
# Forge vhost (#749, mara verdict at issue:9609 — # Forge sub-domain vhost (#749 / #754). `server_name =
# sub-domain over sub-path). When forgejo runs behind the # forge.domain`, proxies all `/` → forgejo. Tuned for
# gateway (`forge.behindGateway = true`), it gets its own # git: `client_max_body_size 1G`, `proxy_read_timeout 1h`
# `server { server_name = forge.domain; }` block. The # (multi-GB clones). SSH stays direct on `forge.sshPort`.
# block proxies all `/` → `http://127.0.0.1:<forge.httpPort>/` # See `docs/gateway.md`.
# 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).
lib.optionalAttrs (forgeCfg.enable or false && forgeCfg.behindGateway or false) { lib.optionalAttrs (forgeCfg.enable or false && forgeCfg.behindGateway or false) {
"${forgeCfg.domain}" = { "${forgeCfg.domain}" = {
listen = [ listen = [
@ -471,34 +325,12 @@ in
}; };
} }
// //
# Matrix homeserver vhost (#747, mara verdict on #749:9609 — # Matrix sub-domain vhost (#747 / #764). `server_name =
# sub-domain over sub-path for matrix; "not user-visible" # matrixCfg.gatewayHost`. `/_matrix/*` → tuwunel (CORS *,
# because clients discover the sub-domain via the # 50M body cap, 1h long-poll timeout). `/` serves
# `.well-known/matrix/{client,server}` delegation served # fluffychat (#772) or 404 if GUI off. nginx
# above on the bare hive-domain). # longer-prefix-wins puts `/_matrix/` ahead of `/`.
# # See `docs/gateway.md`.
# `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.
lib.optionalAttrs (matrixCfg.enable && matrixCfg.gatewayHost != null) { lib.optionalAttrs (matrixCfg.enable && matrixCfg.gatewayHost != null) {
"${matrixCfg.gatewayHost}" = { "${matrixCfg.gatewayHost}" = {
listen = [ listen = [
@ -507,22 +339,48 @@ in
port = cfg.port; port = cfg.port;
} }
]; ];
locations = { locations =
"/_matrix/" = { {
proxyPass = "http://127.0.0.1:${toString matrixCfg.httpPort}"; "/_matrix/" = {
proxyWebsockets = true; proxyPass = "http://127.0.0.1:${toString matrixCfg.httpPort}";
extraConfig = '' proxyWebsockets = true;
proxy_buffering off; extraConfig = ''
client_max_body_size 50M; proxy_buffering off;
proxy_read_timeout 1h; client_max_body_size 50M;
proxy_send_timeout 1h; proxy_read_timeout 1h;
add_header Access-Control-Allow-Origin *; proxy_send_timeout 1h;
''; add_header Access-Control-Allow-Origin *;
'';
};
}
// lib.optionalAttrs (matrixCfg.gui.enable) (
{
# fluffychat at sub-domain root, SPA-fallback via
# the Accept-header `$matrix_spa_target` map.
"/" = {
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";
};
}; };
"/" = {
return = "404";
};
};
}; };
}; };
}; };
@ -533,17 +391,10 @@ in
allowedTCPPorts = [ cfg.port ]; allowedTCPPorts = [ cfg.port ];
}; };
# `/etc/hosts` entries for local dev: the bare hive domain plus # `/etc/hosts` entries for local dev — bare hive domain + any
# any sub-domain modules (forge via #749/#754, matrix via #747) # sub-domain modules that are on. `lib.unique` dedupes if any
# that are on. All map to `127.0.0.1` since the gateway shares # sub-domain happens to equal another. See `docs/gateway.md`
# host netns. Operators with real DNS leave `localHostsEntry = # ("Local dev").
# 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.
networking.hosts = lib.mkIf (cfg.localHostsEntry && hyperhiveDomain != null) { networking.hosts = lib.mkIf (cfg.localHostsEntry && hyperhiveDomain != null) {
"127.0.0.1" = lib.unique ( "127.0.0.1" = lib.unique (
[ hyperhiveDomain ] [ hyperhiveDomain ]

View file

@ -9,50 +9,20 @@ let
hyperhiveDomain = config.services.hyperhive.domain; hyperhiveDomain = config.services.hyperhive.domain;
effectiveServerName = if cfg.serverName != null then cfg.serverName else hyperhiveDomain; effectiveServerName = if cfg.serverName != null then cfg.serverName else hyperhiveDomain;
# Three files are missing from nixpkgs's `pkgs.fluffychat-web` dist # Three files are missing from `pkgs.fluffychat-web` because
# because `flutter341.buildFlutterApplication` doesn't run the dart # `flutter341.buildFlutterApplication` doesn't run the dart
# web-worker compile pass + doesn't run the native_imaging package's # web-worker compile pass + doesn't run the native_imaging emscripten
# emscripten build (#685): # build (#685). `fluffychat-web-imaging` below builds the latter from
# # source via `pkgs.emscripten`; the worker compile is inline in
# - native_executor.js ← flutter web worker entry, compiled # `fluffychat-web-fixed.postInstall`. Drop both when nixpkgs's
# from web/native_executor.dart via # flutter builder grows worker + emcc support upstream.
# `dart compile js` (handled inline
# in `fluffychat-web-fixed.postInstall`
# below — dart SDK is already in the
# flutter341 closure)
#
# - Imaging.js / Imaging.wasm ← emscripten-compiled C library from
# the native_imaging dart package
# (vendored by Famedly). The package
# ships C source + a Makefile that
# builds them via emcc; nixpkgs's
# flutter builder doesn't run that
# pipeline. Built from source via
# `fluffychat-web-imaging` below
# (per mara's #685 call: "fix the
# compile … dont use the prebuilt
# binary").
#
# When `flutter341.buildFlutterApplication` grows worker + emcc
# support upstream, drop both this derivation and the postInstall.
# Imaging.{js,wasm} built from source: native_imaging's `js/Makefile` # `Imaging.{js,wasm}` built from `native_imaging`'s C source via
# runs `emcmake cmake` → `make -C build` → `emcc` to produce the # emscripten. Source comes from
# emscripten-wrapped C library that fluffychat's main.dart.js # `pkgs.fluffychat-web.passthru.pubspecLock.dependencySources` so
# references at runtime. # there's no parallel hash pin — version auto-syncs with nixpkgs
# # bumps. Build closure +~3.6 GiB (emscripten LLVM); runtime closure
# Source: the exact native_imaging derivation that `pkgs.fluffychat-web` # is just the two output files.
# already pulls in via its `pubspecLock` (resolved by nixpkgs's flutter
# pub-cache machinery), reached via `passthru.pubspecLock.dependencySources`.
# This means **no parallel hash pin** — when nixpkgs bumps
# `pkgs.fluffychat-web` (and with it the pubspec.lock-resolved
# native_imaging version), our build automatically picks up the
# matching source. Version is also pulled from passthru for the
# derivation's `version` attr so it stays in lockstep.
#
# Closure cost: `pkgs.emscripten` is ~3.6 GiB build-time (LLVM +
# toolchain). Runtime closure is only the two produced files —
# nothing emscripten-shaped survives into the deployed dist.
fluffychat-web-imaging = pkgs.stdenv.mkDerivation { fluffychat-web-imaging = pkgs.stdenv.mkDerivation {
pname = "fluffychat-web-imaging"; pname = "fluffychat-web-imaging";
version = pkgs.fluffychat-web.passthru.pubspecLock.dependencyVersions.native_imaging; version = pkgs.fluffychat-web.passthru.pubspecLock.dependencyVersions.native_imaging;
@ -102,18 +72,10 @@ let
}; };
}; };
# `pkgs.fluffychat-web` with #685's three missing files patched # `pkgs.fluffychat-web` with #685's three missing files patched in
# in via postInstall, plus the existing `--base-href "/matrix/"` # via postInstall. Mount point is `matrix.<hive>/` (#772); upstream
# override (#634) for the sub-path mount. # `--base-href "/"` is correct at sub-domain root, no override.
fluffychat-web-fixed = pkgs.fluffychat-web.overrideAttrs (old: { fluffychat-web-fixed = pkgs.fluffychat-web.overrideAttrs (old: {
# `--base-href "/matrix/"` so relative asset paths resolve
# under the sub-path mount (#634). Upstream default is `/`,
# wrong for hyperhive's `/matrix/` location.
flutterBuildFlags = (old.flutterBuildFlags or [ ]) ++ [
"--base-href"
"/matrix/"
];
# `dart` from the flutter341 closure (already pulled, no # `dart` from the flutter341 closure (already pulled, no
# incremental closure cost) so we can compile the web-worker # incremental closure cost) so we can compile the web-worker
# entry point that buildFlutterApplication skips. # entry point that buildFlutterApplication skips.
@ -122,34 +84,14 @@ let
postInstall = postInstall =
(old.postInstall or "") (old.postInstall or "")
+ '' + ''
# #685: compile web/native_executor.dart → native_executor.js. # `web/...` is relative to build CWD so dart's package_config
# The flutter web bootstrap loads this from /matrix/native_executor.js # walk-up hits buildFlutterApplication's pub-get output (#685
# at startup; without it, main.dart.js logs a network-error and # / #733 fixup — `$src/web/...` would walk up to a read-only
# the SPA renders blank (see #643 for the symptom). # store path with no `.dart_tool/`).
#
# `dart compile js` needs `.dart_tool/package_config.json` to
# resolve `package:matrix/...` and the rest of fluffychat's
# `pubspec.lock` deps. buildFlutterApplication's pub-get step
# writes that file to the build CWD (the unpacked source dir),
# not to `$src` (the read-only nix store path). So we must use
# a relative path that walks up from `web/` to the build CWD
# where pub-get's package_config lives — pointing at
# `$src/web/native_executor.dart` walks up to `$src/`, finds
# no `.dart_tool/`, and fails with `Couldn't resolve the
# package 'matrix'` (mara's first build attempt on #685).
#
# nixpkgs's buildFlutterApplication leaves CWD at the source
# root for postInstall (see `pkgs/development/compilers/flutter/
# build-support/build-flutter-application.nix` — installPhase
# is `cp -r build/web "$out"` with no `cd` first). So `web/...`
# resolves correctly here.
${pkgs.flutter341.dart}/bin/dart compile js \ ${pkgs.flutter341.dart}/bin/dart compile js \
-o $out/native_executor.js \ -o $out/native_executor.js \
web/native_executor.dart web/native_executor.dart
# #685: install Imaging.{js,wasm} built from the native_imaging
# dart package's C source via emscripten (see
# `fluffychat-web-imaging` above for the build-time rationale).
install -m 644 ${fluffychat-web-imaging}/Imaging.js $out/Imaging.js install -m 644 ${fluffychat-web-imaging}/Imaging.js $out/Imaging.js
install -m 644 ${fluffychat-web-imaging}/Imaging.wasm $out/Imaging.wasm install -m 644 ${fluffychat-web-imaging}/Imaging.wasm $out/Imaging.wasm
''; '';
@ -263,49 +205,17 @@ in
''; '';
example = "matrix.example.com"; example = "matrix.example.com";
description = '' description = ''
Public hostname for the matrix homeserver behind the Public hostname for the matrix homeserver behind the gateway.
hive-gateway nginx (#747, mara verdict on #749:9609 — sub-domain Defaults to `matrix.''${services.hyperhive.domain}` (sub-domain
over sub-path for matrix, but **not user-visible** because the shape per mara on #749:9609). Set to `null` to skip the gateway
`.well-known/matrix/{client,server}` redirect routes clients vhost (tuwunel stays direct on `httpPort`). See
through automatically). `docs/gateway.md` for the vhost map + matrix discovery flow,
and the federation port-8448 caveat at the bottom of that doc.
When set + gateway is on, the gateway adds a `server { server_name Note: `gatewayHost` is the API listener hostname (where nginx
= gatewayHost; }` block that proxies `/_matrix/...` proxies `/_matrix/*`); `serverName` is the matrix-identifier
`http://127.0.0.1:''${httpPort}/_matrix/...`. The domain embedded irrevocably in user/room IDs (per #660
`.well-known/matrix/{client,server}` endpoints (served by the default = bare hive-domain). The two are distinct.
gateway at the bare hive-domain) then point at
`http(s)://''${gatewayHost}/` matrix clients automatically
discover + follow that delegation.
Defaults to `matrix.''${services.hyperhive.domain}` when the
hive-domain is set (idiomatic matrix-spec shape `matrix`
labelled under the hive's bare server_name domain). Defaults to
`null` when the hive-domain is unset (gateway vhost not added;
clients reach tuwunel directly on `httpPort`).
Set to a full hostname (`matrix.example.com`,
`homeserver.internal.lan`) for a bespoke vhost shape. Set to
`null` to disable the gateway vhost entirely (tuwunel stays
direct on `httpPort`).
**server_name vs gatewayHost**: `serverName` is the matrix
identifier domain embedded in user/room IDs irrevocably (per
#660 default = bare hive-domain). `gatewayHost` is just where
the API listens behind nginx. The two are different see the
matrix-spec server-discovery flow.
**Federation port caveat**: the `.well-known/matrix/server`
delegation advertises `''${gatewayHost}` with no port suffix
when the gateway listens on 80. Per the matrix federation
spec, peers fall back to port 8448 when no explicit port is
present but the gateway only listens on the configured
`services.hyperhive.gateway.port`. Cross-hive federation
therefore needs either:
- a DNS SRV record (`_matrix._tcp.''${gatewayHost}` port 80),
- or `services.hyperhive.matrix.openFirewall = true` so peers
can reach tuwunel's federation port directly.
Hyperhive is mostly closed/internal, so this rarely bites in
practice but flagging for the federation-curious operator.
''; '';
}; };
@ -384,22 +294,12 @@ in
default = cfg.enable; default = cfg.enable;
defaultText = lib.literalExpression "config.services.hyperhive.matrix.enable"; defaultText = lib.literalExpression "config.services.hyperhive.matrix.enable";
description = '' description = ''
Serve a matrix web client (default `pkgs.fluffychat-web`) as Serve a matrix web client at `matrix.''${services.hyperhive.domain}/`.
a static dist at `/matrix/` via the hive-gateway nginx Requires `gateway.enable` + `matrix.gatewayHost != null`
(#607 / #634). Defaults to whatever (default true / `matrix.<hive>` when hive-domain set). When
`services.hyperhive.matrix.enable` is turning on the off, the dashboard's `M4TR1X ` tab is hidden. See
homeserver gives you the web client by default; set to `docs/gateway.md` for the discovery flow that lets clients
`false` explicitly to opt out of the GUI while keeping auto-find the sub-domain.
the homeserver running for agents. Requires
`services.hyperhive.gateway.enable` (default on); when
gateway is off no one hosts the GUI and the
`M4TR1X ` dashboard tab is hidden.
fluffychat-web supports per-login server pick point it at
the in-host tuwunel URL (`http://localhost:8008` by
default) the first time. The post-#15 nginx-front re-root
(`https://matrix.''${services.hyperhive.domain}`) is tracked
separately in #609.
''; '';
}; };
@ -407,24 +307,15 @@ in
type = lib.types.package; type = lib.types.package;
default = fluffychat-web-fixed; default = fluffychat-web-fixed;
defaultText = lib.literalMD '' defaultText = lib.literalMD ''
`pkgs.fluffychat-web` rebuilt with `--base-href /matrix/` (#634) `pkgs.fluffychat-web` + #685 `postInstall` patch (adds the
and patched via `postInstall` to add the three files three files `flutter341.buildFlutterApplication` skips).
`flutter341.buildFlutterApplication` skips: `native_executor.js`
(compiled via `dart compile js` from `web/native_executor.dart`),
plus `Imaging.js` + `Imaging.wasm` (built from the
`native_imaging` dart package's C source via `pkgs.emscripten`).
See the `let` block in `nix/modules/hive-matrix.nix` for the
full rationale (#685).
''; '';
description = '' description = ''
Static web client dist to serve at `/matrix/`. Defaults to Static web client dist served at `matrix.<hive>/`. Override
`pkgs.fluffychat-web` rebuilt with `--base-href "/matrix/"` to swap fluffychat for hydrogen-web, cinny, element-web, or
so relative asset paths resolve under the sub-path mount an out-of-tree dist any replacement is mounted at the
(#634), plus a `postInstall` patch for #685's three missing sub-domain root with the upstream-default `<base href "/">`,
files. Override to swap for `hydrogen-web` (lightest), no sub-path gymnastics needed.
`cinny` (no threads), `element-web` (heaviest, full
features), or an out-of-tree client dist any replacement
also needs its `<base href>` aligned with the mount path.
''; '';
}; };
}; };