{ hyperhivePackage, hyperhiveFrontend, hyperhiveAssets, hyperhiveFlake, agentBaseToplevel, managerToplevel, }: { pkgs, lib, config, ... }: let cfg = config.services.hyperhive.c0re; 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 `services.hyperhive.forge.enable = false`. hive-matrix is # opt-in (off by default) and asserts that `services.hyperhive.domain` # is set before it can be enabled. imports = [ ./hive-forge.nix ./hive-gateway.nix ./hive-matrix.nix ]; # Top-level hyperhive enable flag. When true, automatically enables # hive-c0re and the on-by-default hyperhive subsystems. options.services.hyperhive.enable = lib.mkEnableOption "hyperhive — the agent swarm coordinator"; # 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.services.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: `services.hyperhive.matrix.serverName` derives from this, defaulting to `matrix.''${services.hyperhive.domain}` when `serverName` is null). 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.hyperhive.c0re = { enable = lib.mkOption { type = lib.types.bool; default = config.services.hyperhive.enable; defaultText = lib.literalExpression "config.services.hyperhive.enable"; description = "Enable hive-c0re coordinator daemon (auto-enabled by services.hyperhive.enable)."; }; package = lib.mkOption { type = lib.types.package; default = hyperhivePackage pkgs.stdenv.hostPlatform.system; defaultText = lib.literalExpression "hyperhive.packages.\${system}.default"; description = '' hyperhive workspace package. Provides `/bin/hive-c0re` (coordinator daemon + admin-socket CLI) and `/bin/hivectl` (operator-facing host CLI for ad-hoc administration; #655). ''; }; frontend = lib.mkOption { type = lib.types.package; default = hyperhiveFrontend pkgs.stdenv.hostPlatform.system; defaultText = lib.literalExpression "hyperhive.packages.\${system}.frontend"; description = '' Bundled frontend dist (see `./nix/frontend.nix`). Output has `dashboard/` and `agent/` subdirectories — hive-c0re serves `dashboard/` via `tower_http::ServeDir` from the path passed in `HIVE_STATIC_DIR`. Override to ship a custom dashboard SPA; the JSON contract (`/api/state`, the SSE streams, the action endpoints) is the source of truth for any replacement. ''; }; assets = lib.mkOption { type = lib.types.package; default = hyperhiveAssets pkgs.stdenv.hostPlatform.system; defaultText = lib.literalExpression "hyperhive.packages.\${system}.assets"; description = '' Bundled static runtime assets (see `./nix/assets.nix`): the project's branding family + the claude system-prompt template + claude-settings JSON. Output has `share/hyperhive/{branding,prompts}/`; passed to hive-c0re's systemd unit via `HIVE_ASSETS_DIR` (`hive_sh4re::assets::*` resolve paths underneath). Override to ship customised branding or prompts without rebuilding the rust derivation. ''; }; hyperhiveFlake = lib.mkOption { type = lib.types.str; default = hyperhiveFlake; defaultText = lib.literalMD "the flake's own store path"; description = '' URL of the hyperhive flake (no fragment). Inlined into each per-agent `flake.nix` at `inputs.hyperhive.url`. The per-agent flake then pulls `hyperhive.nixosConfigurations.agent-base` to build the container. Defaults to this flake's own store path — only override if you want agents tracking a different ref. ''; }; dashboardPort = lib.mkOption { type = lib.types.port; default = 7000; description = "TCP port the hive-c0re dashboard listens on."; }; operatorPronouns = lib.mkOption { type = lib.types.str; default = "she/her"; example = "they/them"; description = '' Operator pronouns, free text. Threaded into every agent container as the `HIVE_OPERATOR_PRONOUNS` env var; the harness substitutes it into the agent / manager system prompt at boot so claude refers to the operator naturally in third person ("ask her", "tell them", etc.). Changes propagate to running agents on the next `↻ R3BU1LD` — forwards as a meta flake env-var bump, no per-agent approval needed. ''; }; preBuildAgentTemplates = lib.mkOption { type = lib.types.bool; default = false; example = true; description = '' Pre-fetch the per-container system closures (agent-base + manager toplevels) into the host's /nix/store as part of this host's NixOS build, instead of letting the first agent spawn do all the work. Closes #97. Enabling this adds roughly the full nixpkgs runtime closure + claude-code + the harness binary to your system closure size (low single-digit GB), but the first `nixos-container start` for any agent then completes in seconds instead of minutes because nothing's left to fetch. Off by default because the toplevels are pinned to `x86_64-linux` (nixos-containers run native arch). Enabling on an aarch64 host would force nix to build the x86 closure via cross or a remote builder, which is rarely what you want. Flip to `true` on an x86_64 host when you care more about first-spawn latency than host store size — or just `nix build ${hyperhiveFlake}#agent-base-toplevel` once manually to warm the store. ''; }; contextWindowTokens = lib.mkOption { type = lib.types.attrsOf lib.types.int; default = { haiku = 200000; sonnet = 1000000; opus = 1000000; }; example = { haiku = 150000; sonnet = 900000; }; description = '' Per-model context-window sizes in tokens. Each key is a model-family short name matched case-insensitively as a substring of the active model name at runtime (e.g. `"sonnet"` matches `"claude-sonnet-4-5"`). The defaults cover the known Anthropic families; add entries for new models or override existing ones here to change the window for all agents at once. Passed to `hive-c0re serve` as JSON and injected into every container's harness service environment as `HIVE_CONTEXT_WINDOW_TOKENS_`. Changes propagate on the next `↻ R3BU1LD` — no per-agent approval needed. ''; }; }; config = lib.mkIf cfg.enable { environment.systemPackages = [ cfg.package pkgs.git ]; # Pull the per-container toplevels into the host system closure # (#97). `system.extraDependencies` adds paths to the system build # without referencing them at runtime — nixos-rebuild fetches / # builds them, they end up in /nix/store, and the first # nixos-container update + start for an agent has nothing left to # do. Gated because the closure is sizeable and pinned to x86_64. system.extraDependencies = lib.optionals cfg.preBuildAgentTemplates [ agentBaseToplevel managerToplevel ]; # Per-container web UIs share the host's network namespace and need # their ports reachable when there's no gateway in front. Manager: # 8000. Sub-agents: 8100..8999 (deterministic hash; see # `lifecycle::agent_web_port`). # # The dashboard port (`cfg.dashboardPort`, default 7000) is *not* # listed here — since #652 the dashboard binds `127.0.0.1` only, # so opening the firewall hole would be a no-op. Remote dashboard # access flows through hive-gateway (default-on); operators who # opt out of the gateway lose external dashboard reach by design — # the surface is privileged (approve / deny / destroy) and must # not be exposed without a real reverse proxy in front. # # When `services.hyperhive.gateway.enable = true` (the default), the # gateway nginx is the sole external entry point and proxies to # `127.0.0.1:7000` etc. internally — leaving the per-agent ports # open in the host firewall would defeat the gateway's "single # front door" story (closes #621). Operators who opt out of the # gateway still get those direct ports opened so the legacy # `http://:8100/` flow works. networking.firewall = lib.mkIf (!config.services.hyperhive.gateway.enable) { allowedTCPPorts = [ 8000 ]; allowedTCPPortRanges = [ { from = 8100; to = 8999; } ]; }; systemd.services.hive-c0re = { description = "hyperhive coordinator daemon"; wantedBy = [ "multi-user.target" ]; path = [ pkgs.git "/run/current-system/sw" ]; environment = { HYPERHIVE_GIT = "${pkgs.git}/bin/git"; # Path to the dashboard static dist. The hive-c0re axum router # serves this via `tower_http::ServeDir` for any path it doesn't # match against an API/action route. HIVE_STATIC_DIR = "${cfg.frontend}/dashboard"; # Path to the static runtime asset tree (branding + claude # prompts). `hive_sh4re::assets::*` reads paths underneath. # `forge.rs` reads the avatar PNGs from here on startup. HIVE_ASSETS_DIR = "${cfg.assets}/share/hyperhive"; } // lib.optionalAttrs config.services.hyperhive.forge.enable { # Agents poll this URL for Forgejo notifications. Derived from # services.hyperhive.forge.{domain,httpPort} so it tracks forge config changes. HIVE_FORGE_URL = "http://${config.services.hyperhive.forge.domain}:${toString config.services.hyperhive.forge.httpPort}"; } // lib.optionalAttrs config.services.hyperhive.matrix.gui.enable { # Availability flag for `/api/state.matrix_gui_enabled`. The # gateway (hive-gateway.nix) does the actual static serving of # fluffychat-web at `/matrix/`; c0re doesn't host the dist # itself (#634, mara on PR #620). This env var just tells the # dashboard chrome whether the GUI is reachable so iris's # `M4TR1X →` tab doesn't show when the GUI is off. HIVE_MATRIX_GUI_ENABLED = "1"; }; # Matrix GUI static serving lives entirely on the hive-gateway # nginx since #609 — when gateway is off the operator opts out # of matrix-GUI serving entirely (mara on PR #620: "if you # disable gateway you have to static host that yourself # somewhere"). c0re no longer touches /matrix/. serviceConfig = { ExecStart = "${cfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --hyperhive-flake ${cfg.hyperhiveFlake} --dashboard-port ${toString cfg.dashboardPort} --operator-pronouns ${lib.escapeShellArg cfg.operatorPronouns} --context-window-tokens ${lib.escapeShellArg (builtins.toJSON cfg.contextWindowTokens)}"; Restart = "on-failure"; RestartSec = 2; RuntimeDirectory = "hyperhive"; RuntimeDirectoryMode = "0750"; RuntimeDirectoryPreserve = "yes"; StateDirectory = "hyperhive"; }; }; }; }