{ pkgs, lib, config, ... }: let cfg = config.hyperhive.matrix; hyperhiveDomain = config.hyperhive.domain; effectiveServerName = if cfg.serverName != null then cfg.serverName else "matrix.${hyperhiveDomain}"; in { # Private Matrix homeserver (matrix-tuwunel — the official conduwuit # successor) for hyperhive agents, wrapped in a nixos-container so it # doesn't fight any existing `services.matrix-*` the operator may # already run on the host. Same shape as `nix/modules/hive-forge.nix`: # shared host netns (`privateNetwork = false`) so agents reach it at # `http://localhost:` (or via the configured server_name # for federation), nixos-container only here for state + systemd-unit # isolation. # # Container name `hive-matrix` (not `h-*`) so the lifecycle scanner # ignores it; operator manages via the standard `nixos-container` CLI. # # Persistent state at `/var/lib/nixos-containers/hive-matrix/var/lib/ # matrix-tuwunel/` (survives container restart / host reboot). To # wipe, destroy the container. # # Initial rollout (#548): federation enabled (needed for multi-hive # swarms; trusted_servers starts empty so no actual federation traffic # leaves until peers are explicitly listed), registration via admin # API only, e2ee disabled per operator call (tracked for follow-up at # #551). options.hyperhive.matrix = { enable = lib.mkOption { type = lib.types.bool; default = false; description = '' Run hive-matrix — a private matrix-tuwunel homeserver (in a nixos-container) for hyperhive agents. Off by default while the integration phases in; flip to `true` once the operator has set `hyperhive.domain` and is ready to onboard agents. ''; }; package = lib.mkOption { type = lib.types.package; default = pkgs.matrix-tuwunel; defaultText = lib.literalExpression "pkgs.matrix-tuwunel"; description = '' matrix-tuwunel package to run inside the container. Defaults to nixpkgs's `pkgs.matrix-tuwunel`. Override to pin a specific upstream if you need an unreleased feature. ''; }; serverName = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "chat.example.org"; description = '' Matrix `server_name` — the host part of every user ID (`@argus:`) and room ID minted on this homeserver. CRITICAL: must be stable from day one because it's embedded irrevocably in the identifiers. Defaults to `matrix.''${hyperhive.domain}` (always a subdomain — keeps the root domain free for the dashboard or forge). Override here only if you need a name that doesn't follow the `matrix.` shape. ''; }; httpPort = lib.mkOption { type = lib.types.port; default = 8008; description = '' TCP port tuwunel serves the matrix client-server API on. Default 8008 is the matrix-spec well-known port. Sits outside hyperhive's claimed ranges (dashboard 7000, manager 8000, sub-agents 8100..8999). Federation listens on `federationPort` separately. ''; }; openFirewall = lib.mkOption { type = lib.types.bool; default = true; description = '' Open `httpPort` in the host firewall. Off when the homeserver should only be reachable from inside the host (e.g. while bringing the integration up before announcing it to other hives). Note: federation (the matrix-spec well-known port 8448) is intentionally not opened here. tuwunel serves the federation API on the same `httpPort` as the client-server API by default; reaching it on 8448 requires either binding tuwunel to that port explicitly OR a reverse-proxy + `.well-known/ matrix/server` delegation, neither of which lives in this module. Add that proxy config alongside whatever serves your dashboard or forge on 443. ''; }; trustedServers = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; example = [ "matrix.org" ]; description = '' List of trusted matrix servers (homeservers whose signing keys this server will fetch identity-server-style). Empty by default — federation is enabled at the protocol level but no peer is trusted until listed here, so the homeserver is effectively closed until the operator declares hive peers explicitly. ''; }; maxRequestSize = lib.mkOption { type = lib.types.ints.positive; default = 20000000; description = '' Maximum size in bytes of a single matrix client request body. Default 20 MB matches the matrix-spec recommendation for media uploads + the upstream tuwunel default. ''; }; }; config = lib.mkIf cfg.enable { # mara on #548: "there is no default, but it is required. add # assertion." — fail eval with a helpful message rather than # spawning a homeserver with a bogus server_name we can never # change later. `hyperhive.domain` is host-wide; matrix derives # the server_name from it (or from `cfg.serverName` if the # operator wants to override). assertions = [ { assertion = hyperhiveDomain != null || cfg.serverName != null; message = '' hyperhive.matrix.enable = true requires either: - hyperhive.domain set to your host's canonical domain (recommended; shared with forge / dashboard), or - hyperhive.matrix.serverName set explicitly. The matrix server_name is embedded into every user ID and room ID on this homeserver — it cannot be changed later without losing every account and chat history. Pick a stable hostname before enabling. ''; } ]; containers.hive-matrix = { autoStart = true; ephemeral = false; # Share host netns — tuwunel's listeners look exactly like # host-side services, no port-forward plumbing, and agent # containers (also host netns) reach it via plain `localhost`. privateNetwork = false; config = { ... }: { system.stateVersion = "26.05"; services.matrix-tuwunel = { enable = true; package = cfg.package; settings.global = { server_name = effectiveServerName; # `address` is `listOf nonEmptyStr` upstream (multi-bind # support). Single-host bind goes through as a one-element list. address = [ "0.0.0.0" ]; # `port` is `listOf port` upstream. Same shape. port = [ cfg.httpPort ]; max_request_size = cfg.maxRequestSize; # Federation enabled at the protocol level so swarms # can be wired up later by extending `trustedServers` # without a homeserver restart. Empty trusted_servers # keeps it effectively closed until peers are listed. allow_federation = true; trusted_servers = cfg.trustedServers; # Registration off — operator seeds agent accounts via # the tuwunel admin API (mirrors the forge pattern; # see `hive-c0re/src/matrix.rs` once #548 PR 2 lands). allow_registration = false; # E2EE disabled in initial rollout per operator call # (#548) — re-enabling tracked at #551. allow_encryption = false; }; }; environment.systemPackages = [ cfg.package ]; }; }; networking.firewall = lib.mkIf cfg.openFirewall { allowedTCPPorts = [ cfg.httpPort ]; }; }; }