diff --git a/CLAUDE.md b/CLAUDE.md index 02ebe01a..8dd9345f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -197,10 +197,18 @@ nix/ modules/hive-c0re.nix systemd service + firewall + git wiring; `contextWindowTokens` attrset (per-model, injected as env vars into all containers); - imports hive-forge.nix + top-level `hyperhive.domain` option + (nullable; required when matrix.enable); + imports hive-forge.nix + hive-matrix.nix modules/hive-forge.nix optional in-container Forgejo (`hyperhive.forge.enable`, default on); Catppuccin Mocha theme via tmpfiles C+ copy + modules/hive-matrix.nix optional in-container matrix-tuwunel + homeserver (`hyperhive.matrix.enable`, + default off); server_name derives from + `hyperhive.domain` (subdomain by default); + federation on (empty trusted_servers), + registration off, e2ee deferred (#551) templates/harness-base.nix shared scaffolding for sub-agents + manager; `hyperhive.model` option (HIVE_DEFAULT_MODEL) templates/agent-base.nix sub-agent nixosConfiguration diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index 467a5f7e..11a51173 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -17,8 +17,32 @@ in { # The forge is part of the standard install — hive-c0re mirrors # every agent's applied config repo into it. On by default; opt out - # with `hyperhive.forge.enable = false`. - imports = [ ./hive-forge.nix ]; + # with `hyperhive.forge.enable = false`. hive-matrix is opt-in (off + # by default) and asserts that `hyperhive.domain` is set before it + # can be enabled. + imports = [ + ./hive-forge.nix + ./hive-matrix.nix + ]; + + # Top-level option shared by any hyperhive subsystem that needs a + # stable hostname (matrix server_name today, forge ROOT_URL likely + # next). Type is nullable + default null so existing operator + # configs that don't set it still evaluate; subsystems that + # actually need it (matrix) assert non-null in their own config + # block with a helpful message. + options.hyperhive.domain = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "darkest.space"; + description = '' + Canonical host domain for hyperhive subsystems that need a + stable name (currently: `hyperhive.matrix.serverName` derives + from this when `useSubdomain = true`). No default — subsystems + that opt to require it assert non-null in their own config and + fail eval with a helpful message if it's missing. + ''; + }; options.services.hive-c0re = { enable = lib.mkEnableOption "hive-c0re — hyperhive coordinator daemon"; @@ -171,7 +195,8 @@ in # serves this via `tower_http::ServeDir` for any path it doesn't # match against an API/action route. HIVE_STATIC_DIR = "${cfg.frontend}/dashboard"; - } // lib.optionalAttrs config.hyperhive.forge.enable { + } + // lib.optionalAttrs config.hyperhive.forge.enable { # Agents poll this URL for Forgejo notifications. Derived from # hyperhive.forge.{domain,httpPort} so it tracks forge config changes. HIVE_FORGE_URL = "http://${config.hyperhive.forge.domain}:${toString config.hyperhive.forge.httpPort}"; diff --git a/nix/modules/hive-matrix.nix b/nix/modules/hive-matrix.nix new file mode 100644 index 00000000..9457f851 --- /dev/null +++ b/nix/modules/hive-matrix.nix @@ -0,0 +1,222 @@ +{ + pkgs, + lib, + config, + ... +}: +let + cfg = config.hyperhive.matrix; + hyperhiveDomain = config.hyperhive.domain; + effectiveServerName = + if cfg.serverName != null then + cfg.serverName + else if cfg.useSubdomain then + "matrix.${hyperhiveDomain}" + else + 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 = "matrix.darkest.space"; + 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}` when `useSubdomain = true` + (the typical path — keeps the root domain free for the + dashboard or forge), or to `''${hyperhive.domain}` itself + when `useSubdomain = false`. Override here only if you need + a name that doesn't follow either pattern. + ''; + }; + + useSubdomain = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + When `serverName` is unset, use `matrix.''${hyperhive.domain}` + rather than `''${hyperhive.domain}` itself. The default is + true because the root domain is usually claimed by the + dashboard or the forge; turn off only if the root domain is + free AND you want matrix to serve from there. + ''; + }; + + 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. + ''; + }; + + federationPort = lib.mkOption { + type = lib.types.port; + default = 8448; + description = '' + TCP port tuwunel serves the matrix server-server (federation) + API on. Default 8448 is the matrix-spec well-known port. + Open in the host firewall when `openFirewall = true`; needed + for multi-hive swarms to talk to each other. + ''; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Open `httpPort` + `federationPort` 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). + ''; + }; + + 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.str; + 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. String-typed to match tuwunel's TOML config + shape (it accepts both integer + size-with-suffix strings). + ''; + }; + }; + + 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 = "25.11"; + services.matrix-tuwunel = { + enable = true; + package = cfg.package; + settings.global = { + server_name = effectiveServerName; + address = "0.0.0.0"; + 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 + cfg.federationPort + ]; + }; + }; +}