hyperhive/nix/modules/hive-c0re.nix
iris 188f3ea5ec chore: scrub #NNN issue references from code comments and nix descriptions
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).
2026-06-01 13:30:52 +02:00

374 lines
16 KiB
Nix

{
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-ci.nix
./hive-forge.nix
./hive-gateway.nix
./hive-matrix.nix
./hive-network.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";
# Canonical hive DNS domain shared by every subsystem that needs a
# stable hostname. Nullable + default null so existing configs
# evaluate unchanged; subsystems that need it (matrix) assert
# non-null in their own config block. Full identity-surface
# context (HYPERHIVE_HIVE_DOMAIN / HIVE_NAME / SWARM_NAME env-var
# chain → identity.rs → claude prompt): docs/conventions.md::
# Hive identity (label + domain + display names).
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. Exposed to agents as
`HYPERHIVE_HIVE_DOMAIN`; consumed by
`hive-ag3nt::identity::hive_domain()` for `<name>@<domain>`
qualified labels.
'';
};
# Human display names for hive + swarm. Distinct from the DNS
# domain above (machine-readable) — see
# docs/conventions.md::Hive identity for the
# domain-vs-name-vs-swarm distinction + the env-var
# propagation chain.
options.services.hyperhive.hiveName = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "pr1ma";
description = ''
Human-readable name of this single-host hive instance.
Distinct from `services.hyperhive.domain` (the machine-
addressable DNS name): the domain may carry the hive name as
its leftmost label by convention, but this option is the
canonical readable identity. Exposed to agents as
`HYPERHIVE_HIVE_NAME`; surfaced in the dashboard chrome and
per-agent system prompt when set. Null falls back to the
default behaviour (chrome shows the domain, prompt doesn't
mention a hive name).
'';
};
options.services.hyperhive.swarmName = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "constellat1on";
description = ''
Human-readable name of the wider swarm this hive belongs to.
Hives at different DNS domains can share a swarm name when
they federate together. Exposed to agents as
`HYPERHIVE_SWARM_NAME`; surfaced in the dashboard chrome and
per-agent system prompt when set.
'';
};
# Peer hives in the same swarm. Each entry declares a remote hive
# reachable from this host. Serialised to JSON and injected as
# `HYPERHIVE_PEERS` into the hive-c0re service and forwarded to agent
# containers via `meta.rs::FORWARDED_VARS`. Consumed by
# `identity.rs::peers()` + the dashboard's `peer_hives` state field
# (feeds the P33RS dashboard tab).
options.services.hyperhive.swarm.peers = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule {
options = {
certFingerprint = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "sha256:abc123...";
description = ''
Expected TLS certificate fingerprint for this peer's HTTPS
endpoint. Null = trust the system CA bundle (for Let's
Encrypt peers). Set to pin a self-signed cert.
'';
};
};
});
default = { };
example = {
"lab.example.com" = { certFingerprint = "sha256:abc123"; };
"edge.corp" = { };
};
description = ''
Peer hives in the same swarm. The attrset key is the peer's DNS
domain used for dashboard links and Matrix federation discovery.
Null `certFingerprint` trusts the system CA bundle; set it to pin
a self-signed TLS cert.
'';
};
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).
'';
};
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_<KEY_UPPER>`. 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
];
# Open the per-agent web-port range when the gateway is *off* —
# otherwise the gateway nginx is the sole external entry point.
# See `docs/gateway.md::Firewall posture (host-level)`.
networking.firewall = lib.mkIf (!config.services.hyperhive.gateway.enable) {
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.domain != null) {
# Identity env vars threaded into c0re's own service env and
# forwarded by meta.rs into every sub-agent's harness env —
# full chain in docs/conventions.md::Hive identity.
HYPERHIVE_HIVE_DOMAIN = config.services.hyperhive.domain;
}
// lib.optionalAttrs (config.services.hyperhive.hiveName != null) {
HYPERHIVE_HIVE_NAME = config.services.hyperhive.hiveName;
}
// lib.optionalAttrs (config.services.hyperhive.swarmName != null) {
HYPERHIVE_SWARM_NAME = config.services.hyperhive.swarmName;
}
// lib.optionalAttrs config.services.hyperhive.forge.enable {
# Loopback for in-cluster calls (agents share host netns;
# external `forge.<hive>` sub-domain isn't DNS-resolvable
# from inside nspawn). See
# `docs/gateway.md::HIVE_FORGE_URL: loopback for in-cluster,
# sub-domain for the operator`.
HIVE_FORGE_URL = "http://127.0.0.1:${toString config.services.hyperhive.forge.httpPort}";
}
// lib.optionalAttrs config.services.hyperhive.matrix.gui.enable {
# Availability flags read by the dashboard's `/api/state`.
# Matrix GUI lives entirely on the gateway nginx (matrix tab
# only shows when both flags are on). Gateway routing detail:
# docs/gateway.md::Vhost map.
HIVE_MATRIX_GUI_ENABLED = "1";
}
// lib.optionalAttrs config.services.hyperhive.gateway.enable {
# When true the dashboard builds same-origin `/agent/<name>/`
# links; when false it falls back to direct `<host>:<port>` TCP.
HIVE_GATEWAY_ENABLED = "1";
}
//
lib.optionalAttrs
(config.services.hyperhive.forge.enable && config.services.hyperhive.forge.behindGateway)
{
# Public URL of the forge vhost served by hive-gateway. The
# dashboard uses this to build browser-facing forge links
# instead of hardcoding `<hostname>:3000`, which breaks when
# the operator accesses the dashboard through the gateway
# (forge sub-domain has no port; direct port URL would be
# wrong). Absent when `behindGateway = false` — dashboard
# falls back to `<hostname>:3000`.
HIVE_FORGE_PUBLIC_URL = "https://${config.services.hyperhive.forge.domain}";
}
// lib.optionalAttrs (config.services.hyperhive.swarm.peers != { }) {
# Peer hives serialised as a JSON array of {domain, cert_fingerprint}
# objects. Consumed by hive-ag3nt::identity::peers() + the dashboard's
# peer_hives StateSnapshot field (P33RS tab). Domain is the attrset key;
# cert_fingerprint is null for CA-trusted peers.
HYPERHIVE_PEERS = builtins.toJSON (
lib.mapAttrsToList (domain: p: {
inherit domain;
cert_fingerprint = p.certFingerprint;
}) config.services.hyperhive.swarm.peers
);
};
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";
};
};
};
}