hyperhive/docs/matrix.md
atlas a8d8159038 docs/matrix.md: extract hive-matrix prose (#718 batch 5)
New top-level matrix doc covering everything that doesn't fit
elsewhere:

- Container shape (nixos-container, shared host netns, name choice,
  state persistence) — sibling to gateway.md::hive-forge container shape.
- Identity vs API listener: serverName vs gatewayHost split with the
  #660 breaking change.
- Default-closed firewall + federation port 8448 caveat.
- Provisioning flow: registration token activation, bind-mount,
  LoadCredential, hive-c0re's per-agent register + access_token
  persistence. Captures #565 first-boot race + #644 / iris 8043
  ownership shape.
- Assertion rationale (serverName, gatewayHost == "").
- fluffychat-web build fixes (#685): Imaging.{js,wasm} emscripten
  derivation + dart compile worker fixup, build-CWD path lesson
  from #685 / #733.
- Sequencing history.

In-code # comments trim to short purpose statements + docs pointers.
description = '' blocks (operator-facing options docs) preserved per
iris #718. ~140 lines removed from hive-matrix.nix.

`nix flake check` clean; `nix fmt` clean.
2026-05-31 15:38:43 +02:00

199 lines
9.1 KiB
Markdown

# hive-matrix
Private Matrix homeserver (matrix-tuwunel — the conduwuit
successor) wrapped in a nixos-container, plus optional fluffychat-web
client at `matrix.<hive>/`. Configured via
`services.hyperhive.matrix.*`; vhost routing lives in
[`gateway.md`](gateway.md).
## Container shape
Same shape as [`gateway.md::hive-forge container shape`](gateway.md):
- Container name `hive-matrix` (not `h-*`) so c0re's lifecycle
scanner ignores it; operator manages via the standard
`nixos-container` CLI.
- Keeps hive-matrix from fighting any `services.matrix-*` the
operator already runs on the host — separate systemd namespace,
separate state dir.
- Container shares the host network namespace
(`privateNetwork = false`) so agents reach tuwunel at
`http://localhost:<httpPort>` without extra plumbing — the
nixos-container is here for state + systemd-unit isolation, not
network isolation.
- Persistent state at
`/var/lib/nixos-containers/hive-matrix/var/lib/matrix-tuwunel/`
survives container restart / host reboot. To wipe, destroy the
container.
## Identity vs API listener: `serverName` vs `gatewayHost`
Two distinct hostnames:
- **`serverName`** — matrix-spec `server_name`, embedded
*irrevocably* in every `@user:<server_name>` and `!room:<server_name>`
identifier minted on this homeserver. Cannot be changed later
without abandoning every account and chat history. Defaults to the
bare `services.hyperhive.domain` per mara on #660; clients
auto-discover the actual API endpoint via the
`.well-known/matrix/{client,server}` routes the hive-gateway serves
at that domain.
- **`gatewayHost`** — the API listener hostname, where the gateway's
matrix vhost proxies `/_matrix/*` to tuwunel. Defaults to
`matrix.<services.hyperhive.domain>` (sub-domain shape per mara on
#749:9609). Set to `null` to skip the gateway vhost (tuwunel stays
direct on `httpPort`).
**Breaking change** (#660): `serverName` used to default to
`matrix.${services.hyperhive.domain}`. Existing homeservers must set
the option explicitly to preserve their pre-#660 user / room IDs
before rebuilding. The default flipped because the bare hive-domain
makes for cleaner matrix IDs and `.well-known` delegation hides the
sub-domain from the user-facing identifier.
## Default-closed firewall
`openFirewall` defaults to `false` (#651, secure-by-default): the
homeserver is reachable from the host + every agent container via
loopback either way (shared netns), so the firewall hole only
matters for access from *outside* the host. Flip to `true` when
announcing the homeserver to other hives or when an external matrix
client needs to reach the client-server API directly.
**Breaking change** (#651): used to default to `true`. Operators
relying on external reach must add
`services.hyperhive.matrix.openFirewall = true;` before rebuilding.
Federation port 8448 is intentionally not opened here — tuwunel
serves the federation API on the same `httpPort` as client-server
by default. Reaching it on 8448 needs either an explicit tuwunel
bind to that port OR a reverse-proxy + `.well-known/matrix/server`
delegation (the latter lives in `gateway.md::Discovery flow`).
## Provisioning flow (registration token)
Token-gated registration: hive-c0re holds the token, agents never
see it. The agent only receives the resulting `access_token`.
1. **System activation** writes a 32-byte random hex token (64
chars) to `cfg.registrationTokenFile`
(`/var/lib/hyperhive/matrix-register-token` by default), mode
`0600 root:root`, before any container start. Idempotent — only
writes when the file is missing or empty; always re-applies 0600
(normalises any 0640 / world-readable carry-over from
pre-LoadCredential deployments). This runs at activation time
(not first container start) to dodge the argus #565 race where
nspawn creates an empty file when the bind-mount target is
missing and tuwunel reads `registration_token_file=""` rejecting
every registration until next restart.
2. **Read-only bind-mount** maps the host file into the tuwunel
container at the same path.
3. **systemd `LoadCredential=`** inside the container copies the
bind-mounted file into
`/run/credentials/tuwunel.service/registration_token`, owned by
tuwunel's dynamic user with mode `0400`, at service start. The
host file stays `root:root 0600` — no `chown :tuwunel` /
`chmod 0640` / GID-pin gymnastics required (per iris on #644
8043, dropping the shape #649 originally shipped with). Keeps
`DynamicUser = true` + `PrivateUsers = true` intact.
4. tuwunel's `registration_token_file` points at the credentials
path, not the original bind-mount path.
5. **hive-c0re** uses the token to register each agent account via
the matrix-spec UIAA registration flow, persists the returned
`access_token` to `<agent-state>/matrix-token`. The agent's
matrix MCP client authenticates with that access_token and
never touches the shared registration token.
Initial rollout settings (#548):
- `allow_federation = true` at the protocol level so swarms can be
wired up later by extending `trustedServers` without a homeserver
restart. `trusted_servers = []` keeps it effectively closed
until peers are listed.
- `allow_registration = true` (required for the token flow to
engage). The absent
`yes_i_am_very_very_sure_…_open_registration_…` flag keeps the
server closed to anyone without the token.
- `allow_encryption = false` per operator call (#548). E2EE
re-enabling tracked at #551.
## Assertion rationale
Two `config.assertions` entries fail eval early rather than ship
surprising behaviour:
- **`hyperhiveDomain != null || cfg.serverName != null`** (mara on
#548) — `server_name` is embedded into every user / room ID
irrevocably; we refuse to spawn the homeserver with a bogus
server_name we can never change later.
- **`cfg.gatewayHost != ""`** (argus 🟡 on #764) — same footgun as
`forge.domain` (#754). Empty string renders `.<hive>`-shaped
garbage in both nginx `server_name` (treated as wildcard
catch-all, surprising) and `/etc/hosts` (invalid entry). `null`
is the right opt-out shape; empty string is rejected explicitly.
## fluffychat-web build fixes (#685)
`pkgs.fluffychat-web` ships from `flutter341.buildFlutterApplication`,
which has two upstream gaps for fluffychat's web target:
- The dart web-worker entry point (`web/native_executor.dart`) isn't
compiled — `buildFlutterApplication` only runs `flutter build web`
on the main entry.
- `native_imaging`'s C source isn't built — emscripten isn't a
flutter-builder native build input.
Both fixed in `nix/modules/hive-matrix.nix` via two derivations:
- **`fluffychat-web-imaging`** builds `Imaging.{js,wasm}` from the
`native_imaging` C source via `pkgs.emscripten`. Source comes
from `pkgs.fluffychat-web.passthru.pubspecLock.dependencySources.native_imaging`
— already in the build closure of the flutter app, so no parallel
hash pin and version auto-syncs with nixpkgs bumps. Build closure
is ~3.6 GiB (emscripten LLVM); runtime closure is just the two
output files. `dontConfigure = true` because cmake runs inside
`js/Makefile` via `emcmake cmake`, not at the package root. The
build script needs `HOME` + `EM_CACHE` writable for emscripten's
on-demand sysroot build (libc, libc++ → wasm).
- **`fluffychat-web-fixed`** is `pkgs.fluffychat-web` plus a
`postInstall` patch that (a) compiles `web/native_executor.dart`
via `dart compile js` (dart from the flutter341 closure, no
incremental cost) and (b) installs `fluffychat-web-imaging`'s
outputs into `$out`.
Two non-obvious fixes from review history:
- **`make -C js`** instead of `cd js; make` (argus 🟡 on #697 v2)
— keeps the build-phase pwd at the source root so `installPhase`
doesn't have to know about the cd. Robust against future
reorders / `dontBuild`.
- **`web/native_executor.dart`** as a build-CWD-relative path,
*not* `$src/web/...` (#685 / #733 fixup) — `dart`'s
`package_config.json` walk-up needs to hit
`buildFlutterApplication`'s pub-get output (`.dart_tool/` in the
build CWD). Walking up from a read-only `$src/` store path finds
no `.dart_tool/` and errors with "Couldn't resolve the package
'matrix'". Confused two PRs.
Drop both derivations when nixpkgs's flutter builder grows worker
+ emcc support upstream.
Mount point is `matrix.<hive>/` (#772); upstream `--base-href "/"`
is correct at sub-domain root, no override.
## Sequencing history
- #548 — initial rollout (federation enabled, registration enabled,
E2EE disabled)
- #565 — first-boot empty-token race fix → activation-time token
generation
- #644 / iris 8043 / #649 — registration token ownership shape
(dropped chown/GID-pin; LoadCredential delivers as 0400 dynamic-user)
- #651`openFirewall` default flipped to `false`
- #660`serverName` default flipped to bare hive-domain (was
`matrix.<hive>`)
- #685 / #697 / #733 — fluffychat-web build fixes (Imaging emscripten,
native_executor dart worker, build-CWD path)
- #736 — fluffychat config.json inline JSON at sub-domain root
- #749 / #764 — gateway sub-domain shape verdict
- #772 / #775 — fluffychat hops from `<hive>/matrix/` to `matrix.<hive>/`