Part of the docs-migration chore (issue #708). Remove GitHub issue numbers from inline comments, option descriptions, and rustdoc — these are contextless noise for anyone reading the code without access to the original discussions. Replace with prose that captures the same rationale directly. No functional change. Build still clean (cargo check passes).
380 lines
15 KiB
Nix
380 lines
15 KiB
Nix
{
|
|
pkgs,
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.services.hyperhive.matrix;
|
|
hyperhiveDomain = config.services.hyperhive.domain;
|
|
effectiveServerName = if cfg.serverName != null then cfg.serverName else hyperhiveDomain;
|
|
|
|
# fluffychat-web build fixes: nixpkgs's `flutter341.buildFlutterApplication`
|
|
# skips the dart web-worker compile + the emscripten native_imaging
|
|
# build. Two derivations below cover both. Full rationale (why
|
|
# passthru.pubspecLock.dependencySources, why `dontConfigure`, why
|
|
# `make -C js`, why build-CWD-relative dart path): docs/matrix.md::
|
|
# fluffychat-web build fixes.
|
|
|
|
fluffychat-web-imaging = pkgs.stdenv.mkDerivation {
|
|
pname = "fluffychat-web-imaging";
|
|
version = pkgs.fluffychat-web.passthru.pubspecLock.dependencyVersions.native_imaging;
|
|
src = pkgs.fluffychat-web.passthru.pubspecLock.dependencySources.native_imaging;
|
|
|
|
nativeBuildInputs = with pkgs; [
|
|
emscripten
|
|
cmake
|
|
gnumake
|
|
jq
|
|
];
|
|
|
|
# cmake runs inside js/Makefile via `emcmake cmake`; the default
|
|
# configurePhase would invoke cmake at the package root (no
|
|
# CMakeLists) and fail.
|
|
dontConfigure = true;
|
|
|
|
buildPhase = ''
|
|
runHook preBuild
|
|
# emscripten on-demand sysroot build needs writable HOME + cache.
|
|
export HOME=$TMPDIR
|
|
export EM_CACHE=$TMPDIR/.emscriptencache
|
|
mkdir -p $EM_CACHE
|
|
# `make -C js` keeps pwd at source root for the installPhase.
|
|
make -C js Imaging.js Imaging.wasm
|
|
runHook postBuild
|
|
'';
|
|
|
|
installPhase = ''
|
|
runHook preInstall
|
|
mkdir -p $out
|
|
install -m 644 js/Imaging.js $out/Imaging.js
|
|
install -m 644 js/Imaging.wasm $out/Imaging.wasm
|
|
runHook postInstall
|
|
'';
|
|
|
|
meta = with pkgs.lib; {
|
|
description = "Imaging.js + Imaging.wasm built from the native_imaging dart package for fluffychat-web";
|
|
homepage = "https://pub.dev/packages/native_imaging";
|
|
license = licenses.agpl3Plus;
|
|
};
|
|
};
|
|
|
|
fluffychat-web-fixed = pkgs.fluffychat-web.overrideAttrs (old: {
|
|
# dart from the flutter341 closure (already pulled, no incremental
|
|
# cost) to compile the web-worker entry point.
|
|
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ pkgs.flutter341.dart ];
|
|
|
|
postInstall = (old.postInstall or "") + ''
|
|
# `web/...` is BUILD-CWD-relative (not `$src/...`) so dart's
|
|
# package_config walk-up hits buildFlutterApplication's
|
|
# pub-get output `.dart_tool/`.
|
|
${pkgs.flutter341.dart}/bin/dart compile js \
|
|
-o $out/native_executor.js \
|
|
web/native_executor.dart
|
|
|
|
install -m 644 ${fluffychat-web-imaging}/Imaging.js $out/Imaging.js
|
|
install -m 644 ${fluffychat-web-imaging}/Imaging.wasm $out/Imaging.wasm
|
|
'';
|
|
});
|
|
in
|
|
{
|
|
# Private matrix-tuwunel homeserver wrapped in a nixos-container,
|
|
# optional fluffychat-web client at matrix.<hive>/. Container shape,
|
|
# serverName vs gatewayHost split, provisioning flow (registration
|
|
# token + LoadCredential), assertion rationale, initial rollout
|
|
# settings: docs/matrix.md. Vhost map + discovery flow + tuning
|
|
# knobs: docs/gateway.md.
|
|
|
|
options.services.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 `services.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:<server_name>`) and room ID minted on this
|
|
homeserver. CRITICAL: must be stable from day one because
|
|
it's embedded irrevocably in the identifiers. Defaults to
|
|
`services.hyperhive.domain` (the bare hive domain). Combined
|
|
with the `.well-known/matrix/{client,server}` routes the
|
|
hive-gateway serves at that domain, clients auto-discover the
|
|
actual matrix endpoint without needing a subdomain. Override
|
|
here only if you need a different server_name shape (e.g.
|
|
`matrix.<domain>` if you want the subdomain split, or
|
|
`chat.example.org` for a bespoke hostname).
|
|
|
|
**Breaking change**: this used to default to
|
|
`matrix.''${services.hyperhive.domain}`. matrix IDs embed
|
|
the server_name irrevocably, so existing homeservers must
|
|
set `services.hyperhive.matrix.serverName = "matrix.''${services.hyperhive.domain}";`
|
|
explicitly to preserve their existing user / room IDs
|
|
before rebuilding.
|
|
'';
|
|
};
|
|
|
|
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, every
|
|
agent in 8100..8999 via FNV-1a hash). Federation listens on
|
|
`federationPort` separately.
|
|
'';
|
|
};
|
|
|
|
gatewayHost = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = if hyperhiveDomain != null then "matrix.${hyperhiveDomain}" else null;
|
|
defaultText = lib.literalExpression ''
|
|
if services.hyperhive.domain != null then
|
|
"matrix.''${services.hyperhive.domain}"
|
|
else
|
|
null
|
|
'';
|
|
example = "matrix.example.com";
|
|
description = ''
|
|
Public hostname for the matrix homeserver behind the gateway.
|
|
Defaults to `matrix.''${services.hyperhive.domain}` (sub-domain
|
|
shape — see `docs/gateway.md`). Set to `null` to skip the
|
|
gateway vhost (tuwunel stays direct on `httpPort`). See
|
|
`docs/gateway.md` for the vhost map + matrix discovery flow,
|
|
and the federation port-8448 caveat at the bottom of that doc.
|
|
|
|
Note: `gatewayHost` is the API listener hostname (where nginx
|
|
proxies `/_matrix/*`); `serverName` is the matrix-identifier
|
|
domain embedded irrevocably in user/room IDs (default = bare
|
|
hive-domain). The two are distinct.
|
|
'';
|
|
};
|
|
|
|
openFirewall = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
example = true;
|
|
description = ''
|
|
Open `httpPort` in the host firewall. Off by default
|
|
(secure-by-default): the homeserver is reachable from the
|
|
host + every agent container via `localhost` either way
|
|
(shared netns), so the firewall open 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**: this used to default to `true`. If you
|
|
relied on the old default for external reach, add
|
|
`services.hyperhive.matrix.openFirewall = true;` to your host
|
|
config before rebuilding.
|
|
|
|
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.
|
|
'';
|
|
};
|
|
|
|
registrationTokenFile = lib.mkOption {
|
|
type = lib.types.path;
|
|
default = "/var/lib/hyperhive/matrix-register-token";
|
|
description = ''
|
|
Host path to a file containing the matrix registration token
|
|
tuwunel reads to authorise new-account creation. The token is
|
|
generated automatically by `hive-c0re` on first boot (32-byte
|
|
random hex, mode 0600) and is bind-mounted read-only into the
|
|
tuwunel container at the same path. Agents never see this
|
|
token — hive-c0re uses it to provision per-agent accounts
|
|
and the agent only receives the resulting `access_token`.
|
|
Override only when integrating with externally-managed
|
|
registration tokens.
|
|
'';
|
|
};
|
|
|
|
gui = {
|
|
enable = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = cfg.enable;
|
|
defaultText = lib.literalExpression "config.services.hyperhive.matrix.enable";
|
|
description = ''
|
|
Serve a matrix web client at `matrix.''${services.hyperhive.domain}/`.
|
|
Requires `gateway.enable` + `matrix.gatewayHost != null`
|
|
(default true / `matrix.<hive>` when hive-domain set). When
|
|
off, the dashboard's `M4TR1X →` tab is hidden. See
|
|
`docs/gateway.md` for the discovery flow that lets clients
|
|
auto-find the sub-domain.
|
|
'';
|
|
};
|
|
|
|
package = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = fluffychat-web-fixed;
|
|
defaultText = lib.literalMD ''
|
|
`pkgs.fluffychat-web` with a `postInstall` patch that adds
|
|
the three files `flutter341.buildFlutterApplication` skips.
|
|
'';
|
|
description = ''
|
|
Static web client dist served at `matrix.<hive>/`. Override
|
|
to swap fluffychat for hydrogen-web, cinny, element-web, or
|
|
an out-of-tree dist — any replacement is mounted at the
|
|
sub-domain root with the upstream-default `<base href "/">`,
|
|
no sub-path gymnastics needed.
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
# serverName must exist (irrevocably embedded in user/room IDs);
|
|
# gatewayHost may not be "" (same footgun as forge.domain —
|
|
# nginx rejects an empty server_name). docs/matrix.md::Assertion
|
|
# rationale.
|
|
assertions = [
|
|
{
|
|
assertion = hyperhiveDomain != null || cfg.serverName != null;
|
|
message = ''
|
|
services.hyperhive.matrix.enable = true requires either:
|
|
- services.hyperhive.domain set to your host's canonical domain
|
|
(recommended; shared with forge / dashboard), or
|
|
- services.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.
|
|
'';
|
|
}
|
|
{
|
|
assertion = cfg.gatewayHost == null || cfg.gatewayHost != "";
|
|
message = ''
|
|
services.hyperhive.matrix.gatewayHost = "" is rejected. The
|
|
rendered URLs would be invalid (nginx wildcard catch-all for
|
|
an empty server_name, /etc/hosts rejects empty entries).
|
|
Use `null` to disable the gateway vhost entirely (tuwunel
|
|
stays direct on httpPort), or set a non-empty hostname like
|
|
"matrix.example.com" or "homeserver.internal".
|
|
'';
|
|
}
|
|
];
|
|
|
|
# Activation-time token generation — without this the bind-mount
|
|
# would hand tuwunel an empty file on first boot and break every
|
|
# registration until restart. Idempotent;
|
|
# docs/matrix.md::Provisioning flow.
|
|
system.activationScripts.hive-matrix-register-token = lib.stringAfter [ "var" ] ''
|
|
tokenFile=${lib.escapeShellArg (toString cfg.registrationTokenFile)}
|
|
if [ ! -s "$tokenFile" ]; then
|
|
mkdir -p "$(dirname "$tokenFile")"
|
|
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > "$tokenFile"
|
|
echo >> "$tokenFile"
|
|
echo "hive-matrix: generated registration token at $tokenFile"
|
|
fi
|
|
# Re-apply 0600 (normalises any pre-LoadCredential carry-over).
|
|
chmod 0600 "$tokenFile"
|
|
'';
|
|
|
|
containers.hive-matrix = {
|
|
autoStart = true;
|
|
ephemeral = false;
|
|
# Shared host netns — agents reach tuwunel at localhost:<port>.
|
|
privateNetwork = false;
|
|
# Read-only bind of the host-managed registration token; tuwunel
|
|
# reads it via systemd LoadCredential below (not directly).
|
|
bindMounts.${cfg.registrationTokenFile} = {
|
|
hostPath = cfg.registrationTokenFile;
|
|
isReadOnly = true;
|
|
};
|
|
config =
|
|
{ ... }:
|
|
{
|
|
system.stateVersion = "26.05";
|
|
services.matrix-tuwunel = {
|
|
enable = true;
|
|
package = cfg.package;
|
|
settings.global = {
|
|
server_name = effectiveServerName;
|
|
# `address` + `port` are upstream `listOf` — wrap singles.
|
|
address = [ "0.0.0.0" ];
|
|
port = [ cfg.httpPort ];
|
|
max_request_size = cfg.maxRequestSize;
|
|
# Federation enabled at the protocol level; empty
|
|
# trustedServers keeps it effectively closed.
|
|
allow_federation = true;
|
|
trusted_servers = cfg.trustedServers;
|
|
# Token-gated registration. The absent
|
|
# `yes_i_am_very_very_sure_…_open_registration_…` flag
|
|
# keeps the server closed to anyone without the token.
|
|
allow_registration = true;
|
|
# LoadCredential below copies the host file into a
|
|
# 0400 dynamic-user-owned path; tuwunel reads from there.
|
|
registration_token_file = "/run/credentials/tuwunel.service/registration_token";
|
|
# E2EE disabled in initial rollout; tracked in the issue tracker.
|
|
allow_encryption = false;
|
|
};
|
|
};
|
|
# Keeps DynamicUser=true + PrivateUsers=true intact — no
|
|
# host-side chown :tuwunel / GID-pin gymnastics needed.
|
|
# See `man systemd.exec` → LoadCredential.
|
|
systemd.services.tuwunel.serviceConfig.LoadCredential = [
|
|
"registration_token:${toString cfg.registrationTokenFile}"
|
|
];
|
|
environment.systemPackages = [ cfg.package ];
|
|
};
|
|
};
|
|
|
|
networking.firewall = lib.mkIf cfg.openFirewall {
|
|
allowedTCPPorts = [
|
|
cfg.httpPort
|
|
];
|
|
};
|
|
};
|
|
}
|