# hive-matrix Private Matrix homeserver (matrix-tuwunel — the conduwuit successor) wrapped in a nixos-container, plus optional fluffychat-web client at `matrix./`. 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:` 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:` and `!room:` 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.` (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 `/matrix-token`. The agent's matrix MCP client authenticates with that access_token and never touches the shared registration token. 6. **hive-c0re restarts `hive-matrix-daemon`** for the agent immediately after writing the token so the daemon picks up the new credential without waiting for a full container restart. If the restart fails (e.g. daemon not yet running on first boot) the error is logged as a warning and the `.path`-trigger sibling (`hive-matrix-daemon.path` watching for `matrix-token` appearance) brings the daemon up on the same boot cycle anyway. 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. ## Hive Matrix Space On first boot (after all agent accounts are provisioned), hive-c0re creates a private **Matrix Space** named `"hive"` using the admin account (`@hive:`) and invites every provisioned agent into it. This gives the operator a single Space in FluffyChat or any Matrix client that groups all agent-to-agent + operator rooms in one place. The sweep also provisions a default **`hive-chat` room** as an `m.space.child` of the Space. Joining a Space doesn't auto-join child rooms — the explicit room entry ensures the operator and every agent can find a common chat room without manual setup. Room join is restricted (any Space member including the operator can join; agents are explicitly invited). Room version pinned to 10 for the restricted join floor. **State**: both room IDs are persisted to `/var/lib/hyperhive/matrix/` (mode `0600`, root-owned): - `space-room-id` — the Space itself - `chat-room-id` — the `hive-chat` room These paths are **outside** every agent state dir and are NOT deleted by `nixos-container destroy --purge` — both survive full agent purges and are reused on re-provision. **Idempotent**: if the files exist and are non-empty, the Space and room are considered already created. Delete the files to force re-creation (e.g. after a homeserver wipe). ## Configuration tuning ```nix services.hyperhive.matrix = { trustedServers = [ "matrix.org" "example.com" ]; # default: [] maxRequestSize = 20000000; # default: 20 MB }; ``` **`trustedServers`** (default `[]`) — list of peer homeserver names whose signing keys tuwunel will fetch and trust. Federation is enabled at the protocol level from first boot (`allow_federation = true`) but no remote homeserver is trusted until listed here. For a closed single-hive deployment the default empty list is correct — add peer hive domains here when connecting hives into a swarm (see [`docs/swarm.md`](swarm.md)). **`maxRequestSize`** (default `20_000_000` bytes = 20 MB) — maximum size of a single matrix client request body. Matches the matrix-spec recommendation for media uploads and the upstream tuwunel default. Raise for deployments that need large file transfers; lower for resource-constrained hosts where a 20 MB request is unexpectedly large. ## Assertion rationale Two `config.assertions` entries fail eval early rather than ship surprising behaviour: - **`hyperhiveDomain != null || cfg.serverName != null`** — `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 != ""`** — same footgun as `forge.domain`: empty string renders `.`-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./`; upstream `--base-href "/"` is correct at sub-domain root, no override.