hyperhive/docs/matrix.md
iris 6bdfe52386 docs: scrub #NNN self-references (closes #719)
Last pass of the docs-from-code → docs/ epic (#708). Drops every
attribution cookie from docs/ + README.md + CLAUDE.md so the
source-tree files no longer reference the issue tracker. Issue
threads + commit history retain the references — those are the
canonical record.

- README.md: drop #701 / #660×2 / #551 from matrix + display-name
  sections, rephrase to convey the semantics directly
- CLAUDE.md: scrub 18 cookies from the file map (#655, #15, #784,
  #832, #444, #425, #361, #548, #598, #539, #544, #589, #701,
  #658, #280, #660, #551, #764, #772, #793, #14, #805)
- docs/agent-hierarchy.md: drop #658 ×3 (per-agent user is the
  current shape, not a transition)
- docs/conventions.md: drop #571 (replaced with a docs xref to
  persistence.md::matrix-avatar-sync)
- docs/gateway.md: scrub vhost-map table cookies + Sub-domain
  rationale + Per-agent unix-socket upstream + Self-signed TLS +
  Firewall posture + HIVE_FORGE_URL + Per-agent error pages
  sections; drop the trailing 'Sequencing history' issue list +
  the 'Next-up' issue-link footnote
- docs/matrix.md: scrub serverName/gatewayHost + Default-closed
  firewall + Provisioning flow + Initial rollout + Assertion
  rationale + fluffychat-web build fixes; drop the trailing
  'Sequencing history' issue list
- docs/network.md: drop 'Why ship before #14' #805 quote +
  Container shape #805 attribution + trailing 'Sequencing history'
  + Cross-references issue links; rename v2 column to 'after netns
  isolation'
- docs/web-ui.md: drop #784 from Container row, replace with a
  docs xref to docs/gateway.md::Per-agent unix-socket upstream

Only remaining #NNN in docs/ is the literal markdown-heading
example in docs/forge.md (`#tag`, `#123`, `#!/bin/bash`)
which demonstrates the renderer's behaviour — not an attribution
cookie.
2026-05-31 17:47:21 +02:00

8.1 KiB

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.

Container shape

Same shape as gateway.md::hive-forge container shape:

  • 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; 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). Set to null to skip the gateway vhost (tuwunel stays direct on httpPort).

Breaking change: serverName used to default to matrix.${services.hyperhive.domain}. Existing homeservers must set the option explicitly to preserve their existing 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 (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: 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 a 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. 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:

  • 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; E2EE re-enabling is deferred to a follow-up.

Assertion rationale

Two config.assertions entries fail eval early rather than ship surprising behaviour:

  • hyperhiveDomain != null || cfg.serverName != nullserver_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 != "" — same footgun as forge.domain: 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

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 — 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/...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'".

Drop both derivations when nixpkgs's flutter builder grows worker

  • emcc support upstream.

Mount point is matrix.<hive>/; upstream --base-href "/" is correct at sub-domain root, no override.