From a539dceab1110b94a76e8a2e6e451d7d631f1925 Mon Sep 17 00:00:00 2001 From: atlas Date: Thu, 1 Oct 2026 09:57:47 +0200 Subject: [PATCH] nix: split hive-matrix into service and deploy-mode files `swarm.matrix` (what the homeserver is to every hive: server name, ports, API URL, gateway host, encryption policy, OIDC client id) moves to nix/host-modules/hive-matrix-service.nix. Everything else -- the `imports` block with its two renames and the removed `sso.enable`, the `deploy.matrix` options, the whole `config` block including `containers.hive-matrix`, and every other let binding -- stays in nix/host-modules/hive-matrix.nix, which default.nix now imports alongside the new file. The `swarm.matrix` block reads three let bindings, and the `config` block reads all three too: `cfg` (`apiUrl` defaults from `cfg.httpPort`), `swarmDomain` (`gatewayHost`'s default) and `deployCfg` (`apiUrl` reads `deployCfg.matrix.enable`). All three are option reads, so each file binds them from `config.services.hyperhive.*`. Nothing is duplicated. A pure move: option paths, option definitions and config are unchanged apart from the comment above `deploy.matrix`, which now names the file `swarm.matrix` lives in. Refs #3742 --- nix/host-modules/default.nix | 1 + nix/host-modules/hive-matrix-service.nix | 191 +++++++++++++++++++++++ nix/host-modules/hive-matrix.nix | 182 +-------------------- 3 files changed, 195 insertions(+), 179 deletions(-) create mode 100644 nix/host-modules/hive-matrix-service.nix diff --git a/nix/host-modules/default.nix b/nix/host-modules/default.nix index 8fbb0c05..e469350d 100644 --- a/nix/host-modules/default.nix +++ b/nix/host-modules/default.nix @@ -20,6 +20,7 @@ ./hive-ci.nix ./hive-forge ./hive-gateway + ./hive-matrix-service.nix ./hive-matrix.nix ./hive-network.nix ./hive-priv.nix diff --git a/nix/host-modules/hive-matrix-service.nix b/nix/host-modules/hive-matrix-service.nix new file mode 100644 index 00000000..bd62a9d1 --- /dev/null +++ b/nix/host-modules/hive-matrix-service.nix @@ -0,0 +1,191 @@ +# The swarm's matrix homeserver as every hive sees it: the name it mints ids +# under, the ports and URLs it is reached on, its encryption policy and the +# OIDC client id it is registered under, identical on every host. What the +# host running it decides, and the container itself, are in ./hive-matrix.nix. +{ + lib, + config, + ... +}: +let + cfg = config.services.hyperhive.swarm.matrix; + swarmDomain = config.services.hyperhive.swarm.domain; + deployCfg = config.services.hyperhive.deploy; +in +{ + options.services.hyperhive.swarm.matrix = { + 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 `services.hyperhive.swarm.domain` (the bare swarm + domain), because **a swarm runs one homeserver** — tying its + identity to a single hive's domain would make relocating the + container between hives look like a different homeserver. + Combined with the `.well-known/matrix/{client,server}` routes + the 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. + `chat.example.org` for a bespoke hostname). + + **Breaking change, and the one on this page that cannot be + undone by rebuilding.** This default has now moved twice — from + `matrix.''${services.hyperhive.domain}`, then to the bare hive + domain, and now to the swarm domain. Every existing homeserver + must pin whichever value it already minted ids under, e.g. + + ```nix + services.hyperhive.swarm.matrix.serverName = + config.services.hyperhive.domain; # or "matrix.''${…domain}" + ``` + + before rebuilding. Adopting a new `server_name` does not rename + the old users and rooms — it strands them, because their ids + still name a homeserver that no longer answers. + ''; + }; + + 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. + ''; + }; + + apiUrl = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = if deployCfg.matrix.enable then "http://127.0.0.1:${toString cfg.httpPort}" else null; + defaultText = lib.literalExpression '' + if services.hyperhive.deploy.matrix.enable + then "http://127.0.0.1:''${toString services.hyperhive.swarm.matrix.httpPort}" + else null + ''; + example = "https://matrix.example.com"; + description = '' + Client-server API base URL **hive-c0re itself** uses to + provision matrix (register agent users, create the hive space + and chat room, invite members). Distinct from the agent-facing + `hyperhive.matrix.url`, which is the gateway vhost handed to + each agent's `hive-matrix-daemon`. + + Defaults to the loopback listener **only when this module is the + thing running tuwunel** — in that case the address is not a + guess, it is where this module just put the container. Set it + explicitly (with `enable = false`) when the homeserver runs on + another machine; "everything on one host" is a special case of + the full deployment, not the assumption. + + `null` means hive-c0re has no homeserver to provision against + and matrix provisioning no-ops. There is deliberately no + fallback compiled into the daemon: an address baked into the + binary is one that builds fine and then talks to the wrong + machine. + ''; + }; + + gatewayHost = lib.mkOption { + type = lib.types.nullOr lib.types.str; + # `chat.` under the SWARM domain — both halves change: a swarm runs + # one homeserver, and the label follows the service rather than the + # protocol. + # + # Total on a null swarm domain so the required-domain assertion in + # hive-network.nix is the thing that fires; see the comment there. + default = if swarmDomain == null then "chat.invalid" else "chat.${swarmDomain}"; + defaultText = lib.literalExpression ''"chat.''${services.hyperhive.swarm.domain}"''; + example = "matrix.example.com"; + description = '' + Public hostname for the matrix homeserver behind the gateway. + Defaults to `chat.''${services.hyperhive.swarm.domain}` — the + swarm's domain, because a swarm runs **one** homeserver. Set to + `null` to skip the gateway vhost (tuwunel stays direct on + `httpPort`). See `docs/networking/gateway.md` for the vhost map + matrix + discovery flow, and the federation port-8448 caveat at the + bottom of that doc. + + ⚠️ **`gatewayHost` and `serverName` are different things, and + they carry very different costs.** `gatewayHost` is the API + listener hostname (where nginx proxies `/_matrix/*`) and is + free to change: it is a routing detail clients rediscover + through `.well-known`. `serverName` is the matrix-identifier + domain embedded **irrevocably** in every user and room id — + adopting a new one is a different homeserver, not a rename. + Both defaults now sit under the swarm domain, but only this + one is safe to move on a running deployment. + + A deployment that was running before this moved keeps its + current name by pinning + `matrix.''${services.hyperhive.domain}` here — exactly what the + old default rendered. + ''; + }; + + allowEncryption = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Server-side switch for matrix end-to-end encryption — sets + tuwunel's `allow_encryption`. Off by default: on the hive-internal + homeserver the operator already controls the transport, so server + E2EE adds key-management overhead (cross-signing, device + verification, undecryptable-message recovery) without a clear + threat-model win for the common single-hive case. Turn on when + agents join encrypted rooms on external / federated homeservers, + or when the operator wants message contents opaque to the + homeserver admin. Independent of the agent matrix client, which + always supports decryption so it can read encrypted rooms it is + invited to regardless of this flag; this option only governs + whether THIS homeserver permits room encryption. + ''; + }; + + # This homeserver always delegates login to the swarm's authelia, as + # an OIDC relying party — matrix SSO (`m.login.sso`), offered + # alongside password login. No toggle: a homeserver in a swarm is a + # client of that swarm's identity provider. + # + # ⚠️ Not to be confused with tuwunel's `oidc_*` settings, which point + # the other way: those make this homeserver an *authorization server* + # for matrix clients. This family makes it a *client* of an external + # identity provider. The two share the protocol's name and answer + # opposite questions. + # + # This **adds** a way in. Password login keeps working: an identity + # provider that can take the homeserver offline when it hiccups is a + # worse homeserver than one with two ways in — which is also what + # makes always-on safe. Making authelia the *only* path is a + # separate, reversible switch (tuwunel's `login_with_password`), + # deliberately not folded in here. + # + # ⚠️ Matrix SSO lives **inside** the homeserver, never behind a + # forward-auth proxy: the client-server API is spoken by non-browser + # clients holding matrix access tokens — every agent's own + # `hive-matrix-daemon` — plus federation, and a proxy in front of + # `/_matrix/` breaks all of it. + sso = { + clientId = lib.mkOption { + type = lib.types.str; + default = "tuwunel"; + description = '' + OAuth2 client id this homeserver identifies itself with. Must + match the `id` of the corresponding entry in + `services.hyperhive.swarm.authelia.oidc.clients`. + + The secret it pairs with is a host path, so it lives at + `services.hyperhive.deploy.matrix.sso.clientSecretFile`. + ''; + }; + }; + }; +} diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix index 8f5d2fd0..0fc0abcc 100644 --- a/nix/host-modules/hive-matrix.nix +++ b/nix/host-modules/hive-matrix.nix @@ -406,185 +406,9 @@ in '') ]; - options.services.hyperhive.swarm.matrix = { - 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 `services.hyperhive.swarm.domain` (the bare swarm - domain), because **a swarm runs one homeserver** — tying its - identity to a single hive's domain would make relocating the - container between hives look like a different homeserver. - Combined with the `.well-known/matrix/{client,server}` routes - the 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. - `chat.example.org` for a bespoke hostname). - - **Breaking change, and the one on this page that cannot be - undone by rebuilding.** This default has now moved twice — from - `matrix.''${services.hyperhive.domain}`, then to the bare hive - domain, and now to the swarm domain. Every existing homeserver - must pin whichever value it already minted ids under, e.g. - - ```nix - services.hyperhive.swarm.matrix.serverName = - config.services.hyperhive.domain; # or "matrix.''${…domain}" - ``` - - before rebuilding. Adopting a new `server_name` does not rename - the old users and rooms — it strands them, because their ids - still name a homeserver that no longer answers. - ''; - }; - - 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. - ''; - }; - - apiUrl = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = if deployCfg.matrix.enable then "http://127.0.0.1:${toString cfg.httpPort}" else null; - defaultText = lib.literalExpression '' - if services.hyperhive.deploy.matrix.enable - then "http://127.0.0.1:''${toString services.hyperhive.swarm.matrix.httpPort}" - else null - ''; - example = "https://matrix.example.com"; - description = '' - Client-server API base URL **hive-c0re itself** uses to - provision matrix (register agent users, create the hive space - and chat room, invite members). Distinct from the agent-facing - `hyperhive.matrix.url`, which is the gateway vhost handed to - each agent's `hive-matrix-daemon`. - - Defaults to the loopback listener **only when this module is the - thing running tuwunel** — in that case the address is not a - guess, it is where this module just put the container. Set it - explicitly (with `enable = false`) when the homeserver runs on - another machine; "everything on one host" is a special case of - the full deployment, not the assumption. - - `null` means hive-c0re has no homeserver to provision against - and matrix provisioning no-ops. There is deliberately no - fallback compiled into the daemon: an address baked into the - binary is one that builds fine and then talks to the wrong - machine. - ''; - }; - - gatewayHost = lib.mkOption { - type = lib.types.nullOr lib.types.str; - # `chat.` under the SWARM domain — both halves change: a swarm runs - # one homeserver, and the label follows the service rather than the - # protocol. - # - # Total on a null swarm domain so the required-domain assertion in - # hive-network.nix is the thing that fires; see the comment there. - default = if swarmDomain == null then "chat.invalid" else "chat.${swarmDomain}"; - defaultText = lib.literalExpression ''"chat.''${services.hyperhive.swarm.domain}"''; - example = "matrix.example.com"; - description = '' - Public hostname for the matrix homeserver behind the gateway. - Defaults to `chat.''${services.hyperhive.swarm.domain}` — the - swarm's domain, because a swarm runs **one** homeserver. Set to - `null` to skip the gateway vhost (tuwunel stays direct on - `httpPort`). See `docs/networking/gateway.md` for the vhost map + matrix - discovery flow, and the federation port-8448 caveat at the - bottom of that doc. - - ⚠️ **`gatewayHost` and `serverName` are different things, and - they carry very different costs.** `gatewayHost` is the API - listener hostname (where nginx proxies `/_matrix/*`) and is - free to change: it is a routing detail clients rediscover - through `.well-known`. `serverName` is the matrix-identifier - domain embedded **irrevocably** in every user and room id — - adopting a new one is a different homeserver, not a rename. - Both defaults now sit under the swarm domain, but only this - one is safe to move on a running deployment. - - A deployment that was running before this moved keeps its - current name by pinning - `matrix.''${services.hyperhive.domain}` here — exactly what the - old default rendered. - ''; - }; - - allowEncryption = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Server-side switch for matrix end-to-end encryption — sets - tuwunel's `allow_encryption`. Off by default: on the hive-internal - homeserver the operator already controls the transport, so server - E2EE adds key-management overhead (cross-signing, device - verification, undecryptable-message recovery) without a clear - threat-model win for the common single-hive case. Turn on when - agents join encrypted rooms on external / federated homeservers, - or when the operator wants message contents opaque to the - homeserver admin. Independent of the agent matrix client, which - always supports decryption so it can read encrypted rooms it is - invited to regardless of this flag; this option only governs - whether THIS homeserver permits room encryption. - ''; - }; - - # This homeserver always delegates login to the swarm's authelia, as - # an OIDC relying party — matrix SSO (`m.login.sso`), offered - # alongside password login. No toggle: a homeserver in a swarm is a - # client of that swarm's identity provider. - # - # ⚠️ Not to be confused with tuwunel's `oidc_*` settings, which point - # the other way: those make this homeserver an *authorization server* - # for matrix clients. This family makes it a *client* of an external - # identity provider. The two share the protocol's name and answer - # opposite questions. - # - # This **adds** a way in. Password login keeps working: an identity - # provider that can take the homeserver offline when it hiccups is a - # worse homeserver than one with two ways in — which is also what - # makes always-on safe. Making authelia the *only* path is a - # separate, reversible switch (tuwunel's `login_with_password`), - # deliberately not folded in here. - # - # ⚠️ Matrix SSO lives **inside** the homeserver, never behind a - # forward-auth proxy: the client-server API is spoken by non-browser - # clients holding matrix access tokens — every agent's own - # `hive-matrix-daemon` — plus federation, and a proxy in front of - # `/_matrix/` breaks all of it. - sso = { - clientId = lib.mkOption { - type = lib.types.str; - default = "tuwunel"; - description = '' - OAuth2 client id this homeserver identifies itself with. Must - match the `id` of the corresponding entry in - `services.hyperhive.swarm.authelia.oidc.clients`. - - The secret it pairs with is a host path, so it lives at - `services.hyperhive.deploy.matrix.sso.clientSecretFile`. - ''; - }; - }; - }; - - # What stays above is what the homeserver IS from any hive's point of view: - # the name it answers to, the ports and URLs it is reached on, and the client - # id it is registered under. What lives here is what the host running it + # `swarm.matrix`, in ./hive-matrix-service.nix, is what the homeserver IS from + # any hive's point of view: the name it answers to, the ports and URLs it is + # reached on, and the client id it is registered under. What lives here is what the host running it # decides — which build it runs, whether it is exposed, which peers it trusts, # how large a request it accepts, and where its host-local secrets sit. Same rule as # ./swarm-victorialogs.nix; `enable` already lives in ./deploy.nix, which also