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).
374 lines
16 KiB
Nix
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";
|
|
};
|
|
};
|
|
};
|
|
}
|