refactor: split hive-c0re module, aggregate host stack in nix/modules
This commit is contained in:
parent
5f9af9fc97
commit
e7689a6804
7 changed files with 596 additions and 546 deletions
61
flake.nix
61
flake.nix
|
|
@ -105,40 +105,41 @@
|
||||||
nixosModules = {
|
nixosModules = {
|
||||||
agent-base = ./nix/templates/agent-base.nix;
|
agent-base = ./nix/templates/agent-base.nix;
|
||||||
ruth = ./nix/templates/manager.nix;
|
ruth = ./nix/templates/manager.nix;
|
||||||
# The hive-c0re module wants `pkgs.hyperhive` for its default
|
# The full host stack (nix/modules/default.nix aggregator) plus
|
||||||
# `services.hyperhive.c0re.package`. To avoid making operators apply an
|
# the package/source wiring from this flake. The wiring is a
|
||||||
# overlay (which would also pollute their host pkgs with our
|
# plain config module setting the `services.hyperhive.c0re.*`
|
||||||
# build), we thread the package straight from this flake's
|
# package options via `lib.mkDefault` — no overlay involved, and
|
||||||
# `packages.<system>.default` via a `hyperhivePackage` argument.
|
# an operator override still wins. Intended usage:
|
||||||
hive-c0re = import ./nix/modules/hive-c0re.nix {
|
|
||||||
hyperhivePackage = system: self.packages.${system}.default;
|
|
||||||
hyperhiveFrontend = system: self.packages.${system}.frontend;
|
|
||||||
hyperhiveAssets = system: self.packages.${system}.assets;
|
|
||||||
hyperhiveXdgIcons = system: self.packages.${system}.xdg-icons;
|
|
||||||
hyperhiveFlake = "${sources.hyperhiveFlakeSource}";
|
|
||||||
# Narrow docs/ source, threaded as its own meta-flake input so
|
|
||||||
# doc edits don't re-hash the whole flake source.
|
|
||||||
hyperhiveDocs = "${sources.hyperhiveDocsSource}";
|
|
||||||
# Per-container toplevels — wired into `system.extraDependencies`
|
|
||||||
# when `services.hyperhive.c0re.preBuildAgentTemplates` is on so the
|
|
||||||
# host system closure pre-fetches the heavy build inputs.
|
|
||||||
# Defined only for x86_64-linux because nixosConfigurations are
|
|
||||||
# hardcoded to that system; the option's default keeps the
|
|
||||||
# extra deps gated so aarch64 hosts don't accidentally pull
|
|
||||||
# them in via cross-build.
|
|
||||||
agentBaseToplevel = self.packages.x86_64-linux.agent-base-toplevel;
|
|
||||||
managerToplevel = self.packages.x86_64-linux.ruth-toplevel;
|
|
||||||
};
|
|
||||||
hive-ci = ./nix/modules/hive-ci.nix;
|
|
||||||
hive-forge = ./nix/modules/hive-forge.nix;
|
|
||||||
# Convenience alias: one import covers the full hyperhive host
|
|
||||||
# stack (hive-c0re + hive-forge, since hive-c0re already pulls
|
|
||||||
# in hive-forge). Intended usage:
|
|
||||||
#
|
#
|
||||||
# imports = [ hyperhive.nixosModules.default ];
|
# imports = [ hyperhive.nixosModules.default ];
|
||||||
# services.hyperhive.enable = true;
|
# services.hyperhive.enable = true;
|
||||||
#
|
#
|
||||||
default = self.nixosModules.hive-c0re;
|
default =
|
||||||
|
{ lib, pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [ ./nix/modules ];
|
||||||
|
services.hyperhive.c0re = {
|
||||||
|
package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||||
|
frontend = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.frontend;
|
||||||
|
assets = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.assets;
|
||||||
|
xdgIcons = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.xdg-icons;
|
||||||
|
hyperhiveFlake = lib.mkDefault "${sources.hyperhiveFlakeSource}";
|
||||||
|
# Narrow docs/ source, threaded as its own meta-flake input
|
||||||
|
# so doc edits don't re-hash the whole flake source.
|
||||||
|
hyperhiveDocs = lib.mkDefault "${sources.hyperhiveDocsSource}";
|
||||||
|
# Per-container toplevels — wired into
|
||||||
|
# `system.extraDependencies` when
|
||||||
|
# `services.hyperhive.c0re.preBuildAgentTemplates` is on so
|
||||||
|
# the host system closure pre-fetches the heavy build
|
||||||
|
# inputs. x86_64-linux only (nixosConfigurations are
|
||||||
|
# hardcoded to that system); the gate keeps aarch64 hosts
|
||||||
|
# from pulling them in via cross-build.
|
||||||
|
agentBaseToplevel = lib.mkDefault self.packages.x86_64-linux.agent-base-toplevel;
|
||||||
|
managerToplevel = lib.mkDefault self.packages.x86_64-linux.ruth-toplevel;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
hive-ci = ./nix/modules/hive-ci.nix;
|
||||||
|
hive-forge = ./nix/modules/hive-forge.nix;
|
||||||
};
|
};
|
||||||
|
|
||||||
nixosConfigurations =
|
nixosConfigurations =
|
||||||
|
|
|
||||||
|
|
@ -37,21 +37,14 @@ let
|
||||||
# Stub host system: every hyperhive subsystem `mkForce false` so
|
# Stub host system: every hyperhive subsystem `mkForce false` so
|
||||||
# heavy build inputs (matrix container, forge, etc.) stay out of
|
# heavy build inputs (matrix container, forge, etc.) stay out of
|
||||||
# the eval — only option *declarations* matter for the doc walk.
|
# the eval — only option *declarations* matter for the doc walk.
|
||||||
# Import hive-c0re.nix from the content-addressed nixSrc with stub
|
# Import the host-module aggregator from the content-addressed
|
||||||
# package args so the eval doesn't depend on self's Rust builds.
|
# nixSrc; the package options (`services.hyperhive.c0re.package`
|
||||||
|
# etc.) carry no in-module defaults, but with hyperhive disabled
|
||||||
|
# nothing reads them, so no stubs are needed.
|
||||||
hostEval = nixosSystem {
|
hostEval = nixosSystem {
|
||||||
system = pkgs.stdenv.hostPlatform.system;
|
system = pkgs.stdenv.hostPlatform.system;
|
||||||
modules = [
|
modules = [
|
||||||
(import "${nixSrc}/modules/hive-c0re.nix" {
|
"${nixSrc}/modules"
|
||||||
hyperhivePackage = _system: pkgs.emptyFile;
|
|
||||||
hyperhiveFrontend = _system: pkgs.emptyFile;
|
|
||||||
hyperhiveAssets = _system: pkgs.emptyDirectory;
|
|
||||||
hyperhiveFlake = "";
|
|
||||||
hyperhiveDocs = "";
|
|
||||||
hyperhiveXdgIcons = _system: pkgs.emptyFile;
|
|
||||||
agentBaseToplevel = pkgs.emptyFile;
|
|
||||||
managerToplevel = pkgs.emptyFile;
|
|
||||||
})
|
|
||||||
(
|
(
|
||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
{
|
{
|
||||||
|
|
|
||||||
25
nix/modules/default.nix
Normal file
25
nix/modules/default.nix
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# The full hyperhive host stack, pulled together in one place — this
|
||||||
|
# is what the flake exports as `nixosModules.default` (wrapped with
|
||||||
|
# the package/source wiring; see flake.nix). One import covers
|
||||||
|
# everything; `services.hyperhive.enable = true` turns the stack on.
|
||||||
|
#
|
||||||
|
# The forge is mandatory — hive-c0re mirrors every agent's applied
|
||||||
|
# config repo into it and it's the canonical store for the meta flake
|
||||||
|
# + `internal/*` repos, so there's no enable toggle; it deploys with
|
||||||
|
# hyperhive itself. hive-matrix is opt-in (off by default). All
|
||||||
|
# subsystems rely on `services.hyperhive.domain`, which is required
|
||||||
|
# (asserted in hive-network.nix) whenever hyperhive is enabled.
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./hyperhive.nix
|
||||||
|
./hive-c0re
|
||||||
|
./hive-ci.nix
|
||||||
|
./hive-forge.nix
|
||||||
|
./hive-gateway
|
||||||
|
./hive-matrix.nix
|
||||||
|
./hive-network.nix
|
||||||
|
./hive-tls.nix
|
||||||
|
./otel.nix
|
||||||
|
./swarm.nix
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
{
|
# The hive-c0re coordinator daemon (unprivileged `hive-core` user) and
|
||||||
hyperhivePackage,
|
# its narrow root helper hive-priv, both socket-activated. Options
|
||||||
hyperhiveFrontend,
|
# under `services.hyperhive.c0re.*`. The package/source options
|
||||||
hyperhiveAssets,
|
# (`package`, `frontend`, `assets`, `xdgIcons`, `hyperhiveFlake`,
|
||||||
hyperhiveFlake,
|
# `hyperhiveDocs`, `agentBaseToplevel`, `managerToplevel`) have no
|
||||||
hyperhiveDocs,
|
# in-module defaults — the flake's `nixosModules.default` wires them
|
||||||
hyperhiveXdgIcons,
|
# to this flake's own package outputs via `lib.mkDefault`, so
|
||||||
agentBaseToplevel,
|
# operator overrides still win and no overlay is involved.
|
||||||
managerToplevel,
|
|
||||||
}:
|
|
||||||
{
|
{
|
||||||
pkgs,
|
pkgs,
|
||||||
lib,
|
lib,
|
||||||
|
|
@ -114,404 +112,6 @@ let
|
||||||
if stylixThemeColors != null then themedFrontend stylixThemeColors else cfg.frontend;
|
if stylixThemeColors != null then themedFrontend stylixThemeColors else cfg.frontend;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# The forge is mandatory — hive-c0re mirrors every agent's applied
|
|
||||||
# config repo into it and it's the canonical store for the meta flake
|
|
||||||
# + `internal/*` repos, so there's no enable toggle; it deploys with
|
|
||||||
# hyperhive itself. hive-matrix is opt-in (off by default). All
|
|
||||||
# subsystems rely on `services.hyperhive.domain`, which is required
|
|
||||||
# (asserted in hive-network.nix) whenever hyperhive is enabled.
|
|
||||||
imports = [
|
|
||||||
./hive-ci.nix
|
|
||||||
./hive-forge.nix
|
|
||||||
./hive-gateway
|
|
||||||
./hive-matrix.nix
|
|
||||||
./hive-network.nix
|
|
||||||
./hive-tls.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. Typed nullOr (default null) so the option always
|
|
||||||
# exists, but it's REQUIRED whenever hyperhive is enabled — an
|
|
||||||
# assertion in hive-network.nix fails eval when it's unset, since
|
|
||||||
# matrix bakes it in on first boot and the gateway/forge/agent URLs all
|
|
||||||
# derive from it (no safe default). 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). **Required** when `services.hyperhive.enable` — eval fails
|
|
||||||
with a helpful message if it's unset (it's baked into matrix on
|
|
||||||
first boot and drives the gateway/forge/agent URLs, with no safe
|
|
||||||
default; changing it later is destructive). 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.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
# Whether this hive runs "ruthless" — with no root/manager agent at all.
|
|
||||||
# When true, hive-c0re skips the root-agent auto-management sweep (create
|
|
||||||
# if missing, restart if present-but-stopped). Some hives don't want a
|
|
||||||
# root agent at all — see issue tracker "scope concept: special agents".
|
|
||||||
options.services.hyperhive.ruthless = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
example = true;
|
|
||||||
description = ''
|
|
||||||
Run this hive "ruthless" — with no root (manager) agent at all (no
|
|
||||||
ruth). When `true`, hive-c0re skips the root-agent auto-management
|
|
||||||
sweep entirely (it otherwise creates the root agent's container when
|
|
||||||
missing and restarts it when present but stopped). Defaults to
|
|
||||||
`false` (the historical behaviour — the root agent is auto-managed
|
|
||||||
as required infrastructure). Exposed to hive-c0re as
|
|
||||||
`HYPERHIVE_RUTHLESS`.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
options.services.hyperhive.github.enable = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = true;
|
|
||||||
example = false;
|
|
||||||
description = ''
|
|
||||||
Hive-wide switch for the per-agent GitHub integration (the `gh` CLI
|
|
||||||
wrapper + git credential helper, per `hyperhive.github.enable`). On by
|
|
||||||
default: every agent gets the integration, inert until a PAT is
|
|
||||||
provisioned via the dashboard credentials tab or `hivectl github
|
|
||||||
set-token`. Set `false` to turn it off for the whole hive --- the
|
|
||||||
meta-flake renderer (`hive-c0re/src/meta.rs`) then injects
|
|
||||||
`hyperhive.github.enable = false` into every agent. Exposed to hive-c0re
|
|
||||||
as `HYPERHIVE_GITHUB_DISABLED` (set only when the integration is off).
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
# Hive-wide OTEL stats export. Set ONCE here at host level; the
|
|
||||||
# meta-flake renderer (`hive-c0re/src/meta.rs::otel_config`) reads the
|
|
||||||
# HYPERHIVE_OTEL_* env exported below off hive-c0re's unit and injects
|
|
||||||
# the matching `hyperhive.otel.*` build-time config into EVERY agent
|
|
||||||
# (mirroring the CA-cert injection), so each agent's harness exports
|
|
||||||
# its own Claude Code stats directly to the collector. There is no
|
|
||||||
# per-agent opt-in — this is the single switch for the whole hive.
|
|
||||||
options.services.hyperhive.otel = {
|
|
||||||
enable = lib.mkEnableOption ''
|
|
||||||
hive-wide export of every agent's Claude Code stats (token usage,
|
|
||||||
cost, tool calls) to an OTLP endpoint via Claude Code's built-in
|
|
||||||
OpenTelemetry. One switch for all agents; each harness exports
|
|
||||||
directly to the collector, so it keeps working even when hive-c0re
|
|
||||||
is down
|
|
||||||
'';
|
|
||||||
|
|
||||||
endpoint = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "";
|
|
||||||
example = "https://collector.example.com/otel";
|
|
||||||
description = ''
|
|
||||||
OTLP collector endpoint, set as `OTEL_EXPORTER_OTLP_ENDPOINT`
|
|
||||||
for every agent. Required when `enable` is true.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
protocol = lib.mkOption {
|
|
||||||
type = lib.types.enum [
|
|
||||||
"http/protobuf"
|
|
||||||
"http/json"
|
|
||||||
"grpc"
|
|
||||||
];
|
|
||||||
default = "http/protobuf";
|
|
||||||
description = ''
|
|
||||||
OTLP wire protocol, set as `OTEL_EXPORTER_OTLP_PROTOCOL`.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
headersCredential = lib.mkOption {
|
|
||||||
# `str`, not `path`: a `path`-typed relative literal is hash-copied
|
|
||||||
# into the world-readable nix store at eval time, defeating the
|
|
||||||
# point. Keep it a string + require an absolute runtime path so the
|
|
||||||
# secret is only ever read from disk by systemd at start.
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "/run/secrets/otel-headers";
|
|
||||||
description = ''
|
|
||||||
Absolute path to an operator-provided secret file whose contents
|
|
||||||
become `OTEL_EXPORTER_OTLP_HEADERS` (e.g.
|
|
||||||
`Authorization=Bearer <token>`). hive-c0re forwards this host
|
|
||||||
file into each agent container's credential store via
|
|
||||||
systemd-nspawn `--load-credential=otel-headers:<path>`; the inner
|
|
||||||
harness unit inherits it by name (`LoadCredential`), so the token
|
|
||||||
is never copied into the nix store, the generated config, a bind
|
|
||||||
mount, or argv. Must be absolute. Leave null if the endpoint
|
|
||||||
needs no auth header. A configured-but-missing file is skipped
|
|
||||||
with a log warning (OTEL still exports, without the auth header).
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
extraResourceAttributes = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "";
|
|
||||||
example = "deployment.environment=prod";
|
|
||||||
description = ''
|
|
||||||
Extra comma-separated entries appended to
|
|
||||||
`OTEL_RESOURCE_ATTRIBUTES` after the built-in
|
|
||||||
`service.name` / `agent` / `hive` / `swarm` labels.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
debug = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
description = ''
|
|
||||||
Emit OTEL SDK diagnostic messages to every agent's stderr by
|
|
||||||
setting `CLAUDE_CODE_OTEL_DIAG_STDERR=1`. Useful when
|
|
||||||
troubleshooting collector connectivity or endpoint config;
|
|
||||||
leave off in normal operation to avoid noise in agent logs.
|
|
||||||
Only meaningful when `enable` is true.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
metricIntervalMs = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.ints.positive;
|
|
||||||
default = null;
|
|
||||||
example = 10000;
|
|
||||||
description = ''
|
|
||||||
Metric export interval in milliseconds, set as
|
|
||||||
`OTEL_METRIC_EXPORT_INTERVAL` for every agent. Claude Code's
|
|
||||||
default is 60000 (60s). Leave `null` to use that default.
|
|
||||||
|
|
||||||
Each agent runs claude as a short-lived per-turn process; claude
|
|
||||||
force-flushes metrics on shutdown, so this is not required for
|
|
||||||
metrics to be exported, but a lower value gives more frequent
|
|
||||||
intermediate flushes within long turns. Cosmetic, not a
|
|
||||||
correctness knob.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# 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:b1946ac92492d2347c6235b4d2611184a3f5b6cae6c19d6e3c2f0a8e7d4c9f12";
|
|
||||||
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.
|
|
||||||
|
|
||||||
Format: the literal `sha256:` followed by exactly 64
|
|
||||||
hex digits (case-insensitive, no colon separators) — the
|
|
||||||
SHA-256 digest of the peer's DER-encoded leaf certificate.
|
|
||||||
Generate with `openssl x509 -noout -fingerprint -sha256`,
|
|
||||||
then strip the colons and prepend `sha256:`. A malformed
|
|
||||||
value is ignored with a warning rather than weakening
|
|
||||||
trust. See docs/swarm.md for the full recipe.
|
|
||||||
|
|
||||||
Scopes only to hive-c0re's own peer HTTPS checks — it does
|
|
||||||
NOT help Matrix federation (tuwunel validates against its
|
|
||||||
container trust bundle). For a self-signed peer whose root
|
|
||||||
CA you want trusted hive-wide (every agent + Matrix
|
|
||||||
federation), set `caCert` below.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
caCert = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.path;
|
|
||||||
default = null;
|
|
||||||
example = "./peers/edge-ca.pem";
|
|
||||||
description = ''
|
|
||||||
Path to this peer hive's root CA certificate (PEM). When
|
|
||||||
set, the CA is embedded (at build time, into the nix store
|
|
||||||
— no runtime file on the host) and trusted **everywhere the
|
|
||||||
hive's own internal CA is**: it rides alongside `hive-ca.pem`
|
|
||||||
in each agent's `security.pki.certificateFiles` (via the
|
|
||||||
meta-flake renderer), and is added to the Matrix homeserver
|
|
||||||
container's trust bundle so tuwunel validates *federation*
|
|
||||||
TLS from a self-signed peer hive whose cert chains to it.
|
|
||||||
This is the CA-trust path that `certFingerprint`
|
|
||||||
(leaf-pinning, c0re-only) can't cover, and is what unblocks
|
|
||||||
Matrix federation with a self-signed peer hive. Trust stays
|
|
||||||
inside the hive (agents + the Matrix container), never the
|
|
||||||
host system trust store. Mutually complementary with
|
|
||||||
`certFingerprint`; set `caCert` for the federation case. See
|
|
||||||
docs/swarm.md.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
wireguardPublicKey = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "base64pubkey=";
|
|
||||||
description = ''
|
|
||||||
WireGuard public key for this peer host. Required when
|
|
||||||
`services.hyperhive.swarm.wireguard.enable = true` and
|
|
||||||
you want this peer reachable over the mesh. Null = TLS-
|
|
||||||
only peering (public internet, no mesh tunnel).
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
wireguardEndpoint = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "203.0.113.1:51820";
|
|
||||||
description = ''
|
|
||||||
WireGuard endpoint for this peer in `host:port` form.
|
|
||||||
Required when the peer host is behind a firewall and
|
|
||||||
this host needs to initiate the tunnel. Null = this host
|
|
||||||
waits for the peer to connect (peer-initiates; peer must
|
|
||||||
have an endpoint pointing back at this host).
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
wireguardAddress = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "10.100.0.2/32";
|
|
||||||
description = ''
|
|
||||||
IP address (with prefix) of the peer host on the
|
|
||||||
WireGuard mesh. Used as the `allowedIPs` for the peer's
|
|
||||||
WireGuard config entry and injected into `HYPERHIVE_PEERS`
|
|
||||||
so hive-c0re can route intra-swarm traffic to the mesh
|
|
||||||
address rather than the public domain. Required to include
|
|
||||||
the peer in the WireGuard mesh (peers missing this field
|
|
||||||
are silently excluded from `wg-hive`).
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
default = { };
|
|
||||||
example = {
|
|
||||||
"lab.example.com" = {
|
|
||||||
certFingerprint = "sha256:b1946ac92492d2347c6235b4d2611184a3f5b6cae6c19d6e3c2f0a8e7d4c9f12";
|
|
||||||
};
|
|
||||||
"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. Add `wireguardPublicKey` + `wireguardAddress`
|
|
||||||
(and optionally `wireguardEndpoint`) to include the peer in the
|
|
||||||
WireGuard mesh when `swarm.wireguard.enable = true`.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
# WireGuard mesh config for the local host.
|
|
||||||
# When enabled, hive-c0re configures a `wg-hive` interface on the host
|
|
||||||
# connecting to all peers that have `wireguardPublicKey` declared.
|
|
||||||
# Peers reachable over the mesh are preferred for inter-hive traffic
|
|
||||||
# (no public TLS round-trip needed); peers without a public key still
|
|
||||||
# work via normal HTTPS.
|
|
||||||
options.services.hyperhive.swarm.wireguard = {
|
|
||||||
enable = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
description = ''
|
|
||||||
Enable the WireGuard inter-hive mesh. When true, a `wg-hive`
|
|
||||||
interface is brought up connecting to all swarm peers that
|
|
||||||
declare a `wireguardPublicKey`. Requires
|
|
||||||
`privateKeyFile` to be set.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
privateKeyFile = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.path;
|
|
||||||
default = null;
|
|
||||||
example = "/etc/wireguard/hive.key";
|
|
||||||
description = ''
|
|
||||||
Path to the host's WireGuard private key file. The file must
|
|
||||||
be readable by root and should have mode 0400. Generate with
|
|
||||||
`wg genkey > /etc/wireguard/hive.key`. Required when
|
|
||||||
`swarm.wireguard.enable = true`.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
address = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "";
|
|
||||||
example = "10.100.0.1/24";
|
|
||||||
description = ''
|
|
||||||
IP address (with prefix) of this host on the WireGuard mesh.
|
|
||||||
Use a /24 (or broader) prefix so the routing table covers all
|
|
||||||
peer /32 routes. Example: `"10.100.0.1/24"` for a 256-host mesh.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
listenPort = lib.mkOption {
|
|
||||||
type = lib.types.port;
|
|
||||||
default = 51820;
|
|
||||||
description = ''
|
|
||||||
UDP port the local WireGuard interface listens on. Must be
|
|
||||||
reachable from peer hosts when they initiate the tunnel.
|
|
||||||
Default: 51820 (standard WireGuard port).
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
persistentKeepalive = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.int;
|
|
||||||
default = 25;
|
|
||||||
example = 25;
|
|
||||||
description = ''
|
|
||||||
Seconds between keepalive packets sent to each peer. Useful
|
|
||||||
when this host (or a peer) is behind NAT — keeps the UDP hole
|
|
||||||
open. Set to null to disable. Default: 25 seconds.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
options.services.hyperhive.c0re = {
|
options.services.hyperhive.c0re = {
|
||||||
enable = lib.mkOption {
|
enable = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
|
|
@ -521,21 +121,22 @@ in
|
||||||
};
|
};
|
||||||
package = lib.mkOption {
|
package = lib.mkOption {
|
||||||
type = lib.types.package;
|
type = lib.types.package;
|
||||||
default = hyperhivePackage pkgs.stdenv.hostPlatform.system;
|
|
||||||
defaultText = lib.literalExpression "hyperhive.packages.\${system}.default";
|
defaultText = lib.literalExpression "hyperhive.packages.\${system}.default";
|
||||||
description = ''
|
description = ''
|
||||||
hyperhive workspace package. Provides `/bin/hive-c0re`
|
hyperhive workspace package. Provides `/bin/hive-c0re`
|
||||||
(coordinator daemon + admin-socket CLI) and `/bin/hivectl`
|
(coordinator daemon + admin-socket CLI) and `/bin/hivectl`
|
||||||
(operator-facing host CLI for ad-hoc administration).
|
(operator-facing host CLI for ad-hoc administration). Wired to
|
||||||
|
this flake's `packages.<system>.default` by
|
||||||
|
`nixosModules.default` (via `lib.mkDefault`, so setting it here
|
||||||
|
wins).
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
frontend = lib.mkOption {
|
frontend = lib.mkOption {
|
||||||
type = lib.types.package;
|
type = lib.types.package;
|
||||||
default = hyperhiveFrontend pkgs.stdenv.hostPlatform.system;
|
|
||||||
defaultText = lib.literalExpression "hyperhive.packages.\${system}.frontend";
|
defaultText = lib.literalExpression "hyperhive.packages.\${system}.frontend";
|
||||||
description = ''
|
description = ''
|
||||||
Bundled frontend dist (see `./nix/frontend.nix`). Output has
|
Bundled frontend dist (see `nix/packages/frontend.nix`). Output
|
||||||
`dashboard/` and `agent/` subdirectories — hive-c0re serves
|
has `dashboard/` and `agent/` subdirectories — hive-c0re serves
|
||||||
`dashboard/` via `tower_http::ServeDir` from the path passed
|
`dashboard/` via `tower_http::ServeDir` from the path passed
|
||||||
in `HIVE_STATIC_DIR`. Override to ship a custom dashboard SPA;
|
in `HIVE_STATIC_DIR`. Override to ship a custom dashboard SPA;
|
||||||
the JSON contract (`/api/state`, the SSE streams, the action
|
the JSON contract (`/api/state`, the SSE streams, the action
|
||||||
|
|
@ -551,16 +152,15 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Internal, read-only: `frontend` re-themed with the active stylix
|
Internal, read-only: `frontend` re-themed with the active stylix
|
||||||
palette (or `frontend` verbatim when unthemed); has `dashboard/`
|
palette (or `frontend` verbatim when unthemed); has `dashboard/`
|
||||||
and `agent/`. Exposed so `hive-gateway.nix` can static-serve
|
and `agent/`. Exposed so the hive-gateway module can static-serve
|
||||||
`dashboard/` as an nginx root instead of proxying to hive-c0re.
|
`dashboard/` as an nginx root instead of proxying to hive-c0re.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
assets = lib.mkOption {
|
assets = lib.mkOption {
|
||||||
type = lib.types.package;
|
type = lib.types.package;
|
||||||
default = hyperhiveAssets pkgs.stdenv.hostPlatform.system;
|
|
||||||
defaultText = lib.literalExpression "hyperhive.packages.\${system}.assets";
|
defaultText = lib.literalExpression "hyperhive.packages.\${system}.assets";
|
||||||
description = ''
|
description = ''
|
||||||
Bundled static runtime assets (see `./nix/assets.nix`): the
|
Bundled static runtime assets (see `nix/packages/assets.nix`): the
|
||||||
project's branding family + the claude system-prompt template +
|
project's branding family + the claude system-prompt template +
|
||||||
claude-settings JSON. Output has `share/hyperhive/{branding,prompts}/`;
|
claude-settings JSON. Output has `share/hyperhive/{branding,prompts}/`;
|
||||||
passed to hive-c0re's systemd unit via `HIVE_ASSETS_DIR`
|
passed to hive-c0re's systemd unit via `HIVE_ASSETS_DIR`
|
||||||
|
|
@ -569,21 +169,30 @@ in
|
||||||
rust derivation.
|
rust derivation.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
xdgIcons = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
defaultText = lib.literalExpression "hyperhive.packages.\${system}.xdg-icons";
|
||||||
|
description = ''
|
||||||
|
XDG icon set + .desktop entries for hyperhive processes (see
|
||||||
|
`nix/packages/hive-xdg-icons.nix`), installed into the host
|
||||||
|
system packages so desktop environments can match hyperhive
|
||||||
|
processes to their icon.
|
||||||
|
'';
|
||||||
|
};
|
||||||
hyperhiveFlake = lib.mkOption {
|
hyperhiveFlake = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = hyperhiveFlake;
|
defaultText = lib.literalMD "the hyperhive flake's own filtered source store path";
|
||||||
defaultText = lib.literalMD "the flake's own store path";
|
|
||||||
description = ''
|
description = ''
|
||||||
URL of the hyperhive flake (no fragment). Inlined into each
|
URL of the hyperhive flake (no fragment). Inlined into each
|
||||||
per-agent `flake.nix` at `inputs.hyperhive.url`. The per-agent
|
per-agent `flake.nix` at `inputs.hyperhive.url`. The per-agent
|
||||||
flake then pulls `hyperhive.nixosConfigurations.agent-base` to
|
flake then pulls `hyperhive.nixosConfigurations.agent-base` to
|
||||||
build the container. Defaults to this flake's own store path —
|
build the container. Wired by `nixosModules.default` to this
|
||||||
only override if you want agents tracking a different ref.
|
flake's own filtered source — only override if you want agents
|
||||||
|
tracking a different ref.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
hyperhiveDocs = lib.mkOption {
|
hyperhiveDocs = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = hyperhiveDocs;
|
|
||||||
defaultText = lib.literalMD "the docs/ tree's own store path";
|
defaultText = lib.literalMD "the docs/ tree's own store path";
|
||||||
description = ''
|
description = ''
|
||||||
URL of the narrow `docs/` source (no fragment). Inlined into the
|
URL of the narrow `docs/` source (no fragment). Inlined into the
|
||||||
|
|
@ -594,6 +203,24 @@ in
|
||||||
instead of rebuilding every agent container.
|
instead of rebuilding every agent container.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
agentBaseToplevel = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
defaultText = lib.literalExpression "hyperhive.packages.x86_64-linux.agent-base-toplevel";
|
||||||
|
description = ''
|
||||||
|
Pre-built agent-base container system closure, pulled into the
|
||||||
|
host system closure when `preBuildAgentTemplates` is on. Wired
|
||||||
|
by `nixosModules.default`; only evaluated when that option is
|
||||||
|
enabled.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
managerToplevel = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
defaultText = lib.literalExpression "hyperhive.packages.x86_64-linux.ruth-toplevel";
|
||||||
|
description = ''
|
||||||
|
Pre-built manager (ruth) container system closure — see
|
||||||
|
`agentBaseToplevel`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
nixpkgsFlake = lib.mkOption {
|
nixpkgsFlake = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "path:${pkgs.path}";
|
default = "path:${pkgs.path}";
|
||||||
|
|
@ -657,8 +284,8 @@ in
|
||||||
via cross or a remote builder, which is rarely what you want.
|
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
|
Flip to `true` on an x86_64 host when you care more about
|
||||||
first-spawn latency than host store size — or just
|
first-spawn latency than host store size — or just
|
||||||
`nix build ${hyperhiveFlake}#agent-base-toplevel` once
|
`nix build .#agent-base-toplevel` once manually to warm the
|
||||||
manually to warm the store.
|
store.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
contextWindowTokens = lib.mkOption {
|
contextWindowTokens = lib.mkOption {
|
||||||
|
|
@ -769,9 +396,8 @@ in
|
||||||
systemd `CPUQuota=` applied to every agent container via a
|
systemd `CPUQuota=` applied to every agent container via a
|
||||||
`container@h-<name>.service.d/` drop-in written on each
|
`container@h-<name>.service.d/` drop-in written on each
|
||||||
spawn/rebuild. Expressed as a percentage of one CPU core —
|
spawn/rebuild. Expressed as a percentage of one CPU core —
|
||||||
`"200%"` allows each agent to use up to 2 cores. The old
|
`"200%"` allows each agent to use up to 2 cores. Bump this if
|
||||||
hard-coded value was `"50%"`; bump this if agents are hitting
|
agents are hitting CPU limits during builds or heavy tool use.
|
||||||
CPU limits during builds or heavy tool use.
|
|
||||||
|
|
||||||
For a hive-wide cap across all containers, set
|
For a hive-wide cap across all containers, set
|
||||||
`systemd.slices.machine.serviceConfig.CPUQuota` in your NixOS
|
`systemd.slices.machine.serviceConfig.CPUQuota` in your NixOS
|
||||||
|
|
@ -785,8 +411,7 @@ in
|
||||||
example = "8G";
|
example = "8G";
|
||||||
description = ''
|
description = ''
|
||||||
systemd `MemoryMax=` applied to every agent container via the
|
systemd `MemoryMax=` applied to every agent container via the
|
||||||
same drop-in as `agentCpuQuota`. The old hard-coded value was
|
same drop-in as `agentCpuQuota`.
|
||||||
`"2G"`.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -798,11 +423,10 @@ in
|
||||||
Number of nix-heavy job-queue nodes (container prebuilds,
|
Number of nix-heavy job-queue nodes (container prebuilds,
|
||||||
profile swaps, first-spawn creates, meta lock bumps) hive-c0re
|
profile swaps, first-spawn creates, meta lock bumps) hive-c0re
|
||||||
runs concurrently. The default of 1 serializes all heavy nix
|
runs concurrently. The default of 1 serializes all heavy nix
|
||||||
work like the pre-DAG rebuild queue did; raise it on hosts with
|
work; raise it on hosts with the cores/RAM to build several
|
||||||
the cores/RAM to build several agent toplevels at once.
|
agent toplevels at once. Per-agent correctness is independent
|
||||||
Per-agent correctness is independent of this count — each
|
of this count — each agent's container-affecting operations are
|
||||||
agent's container-affecting operations are serialized by its
|
serialized by its lifecycle lease regardless.
|
||||||
lifecycle lease regardless.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -813,7 +437,7 @@ in
|
||||||
pkgs.git
|
pkgs.git
|
||||||
# XDG icons + .desktop entries so desktop environments can match
|
# XDG icons + .desktop entries so desktop environments can match
|
||||||
# hyperhive processes to their icon (task managers, CPU monitors, etc.).
|
# hyperhive processes to their icon (task managers, CPU monitors, etc.).
|
||||||
(hyperhiveXdgIcons pkgs.stdenv.hostPlatform.system)
|
cfg.xdgIcons
|
||||||
];
|
];
|
||||||
|
|
||||||
# Serve config at a stable /etc path so hive-c0re's ExecStart
|
# Serve config at a stable /etc path so hive-c0re's ExecStart
|
||||||
|
|
@ -828,8 +452,8 @@ in
|
||||||
# nixos-container update + start for an agent has nothing left to
|
# nixos-container update + start for an agent has nothing left to
|
||||||
# do. Gated because the closure is sizeable and pinned to x86_64.
|
# do. Gated because the closure is sizeable and pinned to x86_64.
|
||||||
system.extraDependencies = lib.optionals cfg.preBuildAgentTemplates [
|
system.extraDependencies = lib.optionals cfg.preBuildAgentTemplates [
|
||||||
agentBaseToplevel
|
cfg.agentBaseToplevel
|
||||||
managerToplevel
|
cfg.managerToplevel
|
||||||
];
|
];
|
||||||
|
|
||||||
# Unprivileged coordinator user. hive-c0re runs as this user;
|
# Unprivileged coordinator user. hive-c0re runs as this user;
|
||||||
|
|
@ -846,76 +470,11 @@ in
|
||||||
# alongside hyperhive), so the per-agent web-port range stays closed on
|
# alongside hyperhive), so the per-agent web-port range stays closed on
|
||||||
# the host firewall. See `docs/gateway.md::Firewall posture (host-level)`.
|
# the host firewall. See `docs/gateway.md::Firewall posture (host-level)`.
|
||||||
|
|
||||||
# WireGuard inter-hive mesh. Enabled when
|
|
||||||
# `services.hyperhive.swarm.wireguard.enable = true`. Brings up a
|
|
||||||
# `wg-hive` interface and connects to each peer that has
|
|
||||||
# `wireguardPublicKey` set. Firewall opens the UDP listen port on
|
|
||||||
# the host (not inside containers — this is host-level networking).
|
|
||||||
networking.wireguard.interfaces = lib.mkIf config.services.hyperhive.swarm.wireguard.enable (
|
|
||||||
let
|
|
||||||
wgCfg = config.services.hyperhive.swarm.wireguard;
|
|
||||||
meshPeers = lib.filterAttrs (
|
|
||||||
_: p: p.wireguardPublicKey != null && p.wireguardAddress != null
|
|
||||||
) config.services.hyperhive.swarm.peers;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
wg-hive = {
|
|
||||||
ips = [ wgCfg.address ];
|
|
||||||
listenPort = wgCfg.listenPort;
|
|
||||||
privateKeyFile = wgCfg.privateKeyFile;
|
|
||||||
peers = lib.mapAttrsToList (
|
|
||||||
_domain: p:
|
|
||||||
{
|
|
||||||
publicKey = p.wireguardPublicKey;
|
|
||||||
allowedIPs = [ p.wireguardAddress ];
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs (p.wireguardEndpoint != null) {
|
|
||||||
endpoint = p.wireguardEndpoint;
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs (wgCfg.persistentKeepalive != null) {
|
|
||||||
persistentKeepalive = wgCfg.persistentKeepalive;
|
|
||||||
}
|
|
||||||
) meshPeers;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
# Open the WireGuard UDP port on the host firewall when the mesh is on.
|
|
||||||
networking.firewall.allowedUDPPorts = lib.mkIf config.services.hyperhive.swarm.wireguard.enable [
|
|
||||||
config.services.hyperhive.swarm.wireguard.listenPort
|
|
||||||
];
|
|
||||||
|
|
||||||
# NB: `services.hyperhive.domain` is required when hyperhive is
|
# NB: `services.hyperhive.domain` is required when hyperhive is
|
||||||
# enabled — the canonical assertion lives in `hive-network.nix` (the
|
# enabled — the canonical assertion lives in `hive-network.nix` (the
|
||||||
# hive resolver is authoritative for `<domain>` and agents reach the
|
# hive resolver is authoritative for `<domain>` and agents reach the
|
||||||
# forge/matrix through the gateway by it). So everything below can
|
# forge/matrix through the gateway by it). So everything below can
|
||||||
# treat `config.services.hyperhive.domain` as non-null.
|
# treat `config.services.hyperhive.domain` as non-null.
|
||||||
assertions =
|
|
||||||
lib.optionals config.services.hyperhive.swarm.wireguard.enable [
|
|
||||||
{
|
|
||||||
assertion = config.services.hyperhive.swarm.wireguard.privateKeyFile != null;
|
|
||||||
message = ''
|
|
||||||
services.hyperhive.swarm.wireguard.enable requires
|
|
||||||
services.hyperhive.swarm.wireguard.privateKeyFile to be set.
|
|
||||||
Generate a key: wg genkey > /etc/wireguard/hive.key
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = config.services.hyperhive.swarm.wireguard.address != "";
|
|
||||||
message = ''
|
|
||||||
services.hyperhive.swarm.wireguard.enable requires
|
|
||||||
services.hyperhive.swarm.wireguard.address to be set
|
|
||||||
(e.g. "10.100.0.1/24").
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
]
|
|
||||||
++ lib.optionals config.services.hyperhive.otel.enable [
|
|
||||||
{
|
|
||||||
assertion = config.services.hyperhive.otel.endpoint != "";
|
|
||||||
message = "services.hyperhive.otel.enable is true but services.hyperhive.otel.endpoint is empty.";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
systemd.services.hive-c0re = {
|
systemd.services.hive-c0re = {
|
||||||
description = "hyperhive coordinator daemon";
|
description = "hyperhive coordinator daemon";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
@ -941,7 +500,7 @@ in
|
||||||
HOME = "/var/lib/hyperhive";
|
HOME = "/var/lib/hyperhive";
|
||||||
HYPERHIVE_GIT = "${pkgs.git}/bin/git";
|
HYPERHIVE_GIT = "${pkgs.git}/bin/git";
|
||||||
# No HIVE_STATIC_DIR: the gateway static-serves the dashboard dist
|
# No HIVE_STATIC_DIR: the gateway static-serves the dashboard dist
|
||||||
# now (see hive-gateway.nix); this router is API-only.
|
# (see the hive-gateway module); this router is API-only.
|
||||||
# Path to the base agent frontend dist. hive-c0re's
|
# Path to the base agent frontend dist. hive-c0re's
|
||||||
# gateway_nginx.rs uses this to generate split location
|
# gateway_nginx.rs uses this to generate split location
|
||||||
# blocks in agents.conf — static HTML/CSS/JS served from the
|
# blocks in agents.conf — static HTML/CSS/JS served from the
|
||||||
|
|
@ -955,8 +514,8 @@ in
|
||||||
# `forge.rs` reads the avatar PNGs from here on startup.
|
# `forge.rs` reads the avatar PNGs from here on startup.
|
||||||
HIVE_ASSETS_DIR = "${cfg.assets}/share/hyperhive";
|
HIVE_ASSETS_DIR = "${cfg.assets}/share/hyperhive";
|
||||||
# Whether this hive runs ruthless — no root/manager agent at all
|
# Whether this hive runs ruthless — no root/manager agent at all
|
||||||
# (`auto_update::ensure_root_agent`). Default false = historical
|
# (`auto_update::ensure_root_agent`). Default false = root
|
||||||
# behaviour (root auto-managed); true makes the sweep a no-op.
|
# auto-managed; true makes the sweep a no-op.
|
||||||
HYPERHIVE_RUTHLESS = lib.boolToString config.services.hyperhive.ruthless;
|
HYPERHIVE_RUTHLESS = lib.boolToString config.services.hyperhive.ruthless;
|
||||||
}
|
}
|
||||||
// {
|
// {
|
||||||
109
nix/modules/hyperhive.nix
Normal file
109
nix/modules/hyperhive.nix
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
# Top-level, cross-cutting hyperhive options: the master enable
|
||||||
|
# switch, the hive's identity (domain + display names), and hive-wide
|
||||||
|
# feature toggles read by several subsystem modules. Imported by the
|
||||||
|
# ./default.nix aggregator.
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
# 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. Typed nullOr (default null) so the option always
|
||||||
|
# exists, but it's REQUIRED whenever hyperhive is enabled — an
|
||||||
|
# assertion in hive-network.nix fails eval when it's unset, since
|
||||||
|
# matrix bakes it in on first boot and the gateway/forge/agent URLs all
|
||||||
|
# derive from it (no safe default). 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). **Required** when `services.hyperhive.enable` — eval fails
|
||||||
|
with a helpful message if it's unset (it's baked into matrix on
|
||||||
|
first boot and drives the gateway/forge/agent URLs, with no safe
|
||||||
|
default; changing it later is destructive). 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.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Whether this hive runs "ruthless" — with no root/manager agent at
|
||||||
|
# all. Some hives don't want a root agent — see issue tracker
|
||||||
|
# "scope concept: special agents".
|
||||||
|
options.services.hyperhive.ruthless = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
example = true;
|
||||||
|
description = ''
|
||||||
|
Run this hive "ruthless" — with no root (manager) agent at all (no
|
||||||
|
ruth). When `true`, hive-c0re skips the root-agent auto-management
|
||||||
|
sweep entirely (it otherwise creates the root agent's container when
|
||||||
|
missing and restarts it when present but stopped). Defaults to
|
||||||
|
`false` (the root agent is auto-managed as required
|
||||||
|
infrastructure). Exposed to hive-c0re as `HYPERHIVE_RUTHLESS`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
options.services.hyperhive.github.enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
example = false;
|
||||||
|
description = ''
|
||||||
|
Hive-wide switch for the per-agent GitHub integration (the `gh` CLI
|
||||||
|
wrapper + git credential helper, per `hyperhive.github.enable`). On by
|
||||||
|
default: every agent gets the integration, inert until a PAT is
|
||||||
|
provisioned via the dashboard credentials tab or `hivectl github
|
||||||
|
set-token`. Set `false` to turn it off for the whole hive --- the
|
||||||
|
meta-flake renderer (`hive-c0re/src/meta.rs`) then injects
|
||||||
|
`hyperhive.github.enable = false` into every agent. Exposed to hive-c0re
|
||||||
|
as `HYPERHIVE_GITHUB_DISABLED` (set only when the integration is off).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
117
nix/modules/otel.nix
Normal file
117
nix/modules/otel.nix
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
# Hive-wide OTEL stats export. Set ONCE here at host level; the
|
||||||
|
# meta-flake renderer (`hive-c0re/src/meta.rs::otel_config`) reads the
|
||||||
|
# HYPERHIVE_OTEL_* env exported off hive-c0re's unit (see
|
||||||
|
# ./hive-c0re) and injects the matching `hyperhive.otel.*` build-time
|
||||||
|
# config into EVERY agent (mirroring the CA-cert injection), so each
|
||||||
|
# agent's harness exports its own Claude Code stats directly to the
|
||||||
|
# collector. There is no per-agent opt-in — this is the single switch
|
||||||
|
# for the whole hive.
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
options.services.hyperhive.otel = {
|
||||||
|
enable = lib.mkEnableOption ''
|
||||||
|
hive-wide export of every agent's Claude Code stats (token usage,
|
||||||
|
cost, tool calls) to an OTLP endpoint via Claude Code's built-in
|
||||||
|
OpenTelemetry. One switch for all agents; each harness exports
|
||||||
|
directly to the collector, so it keeps working even when hive-c0re
|
||||||
|
is down
|
||||||
|
'';
|
||||||
|
|
||||||
|
endpoint = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "";
|
||||||
|
example = "https://collector.example.com/otel";
|
||||||
|
description = ''
|
||||||
|
OTLP collector endpoint, set as `OTEL_EXPORTER_OTLP_ENDPOINT`
|
||||||
|
for every agent. Required when `enable` is true.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
protocol = lib.mkOption {
|
||||||
|
type = lib.types.enum [
|
||||||
|
"http/protobuf"
|
||||||
|
"http/json"
|
||||||
|
"grpc"
|
||||||
|
];
|
||||||
|
default = "http/protobuf";
|
||||||
|
description = ''
|
||||||
|
OTLP wire protocol, set as `OTEL_EXPORTER_OTLP_PROTOCOL`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
headersCredential = lib.mkOption {
|
||||||
|
# `str`, not `path`: a `path`-typed relative literal is hash-copied
|
||||||
|
# into the world-readable nix store at eval time, defeating the
|
||||||
|
# point. Keep it a string + require an absolute runtime path so the
|
||||||
|
# secret is only ever read from disk by systemd at start.
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/run/secrets/otel-headers";
|
||||||
|
description = ''
|
||||||
|
Absolute path to an operator-provided secret file whose contents
|
||||||
|
become `OTEL_EXPORTER_OTLP_HEADERS` (e.g.
|
||||||
|
`Authorization=Bearer <token>`). hive-c0re forwards this host
|
||||||
|
file into each agent container's credential store via
|
||||||
|
systemd-nspawn `--load-credential=otel-headers:<path>`; the inner
|
||||||
|
harness unit inherits it by name (`LoadCredential`), so the token
|
||||||
|
is never copied into the nix store, the generated config, a bind
|
||||||
|
mount, or argv. Must be absolute. Leave null if the endpoint
|
||||||
|
needs no auth header. A configured-but-missing file is skipped
|
||||||
|
with a log warning (OTEL still exports, without the auth header).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
extraResourceAttributes = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "";
|
||||||
|
example = "deployment.environment=prod";
|
||||||
|
description = ''
|
||||||
|
Extra comma-separated entries appended to
|
||||||
|
`OTEL_RESOURCE_ATTRIBUTES` after the built-in
|
||||||
|
`service.name` / `agent` / `hive` / `swarm` labels.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
debug = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Emit OTEL SDK diagnostic messages to every agent's stderr by
|
||||||
|
setting `CLAUDE_CODE_OTEL_DIAG_STDERR=1`. Useful when
|
||||||
|
troubleshooting collector connectivity or endpoint config;
|
||||||
|
leave off in normal operation to avoid noise in agent logs.
|
||||||
|
Only meaningful when `enable` is true.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
metricIntervalMs = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.ints.positive;
|
||||||
|
default = null;
|
||||||
|
example = 10000;
|
||||||
|
description = ''
|
||||||
|
Metric export interval in milliseconds, set as
|
||||||
|
`OTEL_METRIC_EXPORT_INTERVAL` for every agent. Claude Code's
|
||||||
|
default is 60000 (60s). Leave `null` to use that default.
|
||||||
|
|
||||||
|
Each agent runs claude as a short-lived per-turn process; claude
|
||||||
|
force-flushes metrics on shutdown, so this is not required for
|
||||||
|
metrics to be exported, but a lower value gives more frequent
|
||||||
|
intermediate flushes within long turns. Cosmetic, not a
|
||||||
|
correctness knob.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf config.services.hyperhive.c0re.enable {
|
||||||
|
assertions = lib.optionals config.services.hyperhive.otel.enable [
|
||||||
|
{
|
||||||
|
assertion = config.services.hyperhive.otel.endpoint != "";
|
||||||
|
message = "services.hyperhive.otel.enable is true but services.hyperhive.otel.endpoint is empty.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
246
nix/modules/swarm.nix
Normal file
246
nix/modules/swarm.nix
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
# Swarm peering: the peer-hive declarations and the optional
|
||||||
|
# WireGuard inter-hive mesh. The peers are serialised into hive-c0re's
|
||||||
|
# environment (HYPERHIVE_PEERS / HIVE_PEER_CA_PATHS — see ./hive-c0re)
|
||||||
|
# and consumed by identity.rs + the dashboard's P33RS tab; the mesh
|
||||||
|
# config below is host-level networking.
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
# Peer hives in the same swarm. Each entry declares a remote hive
|
||||||
|
# reachable from this host.
|
||||||
|
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:b1946ac92492d2347c6235b4d2611184a3f5b6cae6c19d6e3c2f0a8e7d4c9f12";
|
||||||
|
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.
|
||||||
|
|
||||||
|
Format: the literal `sha256:` followed by exactly 64
|
||||||
|
hex digits (case-insensitive, no colon separators) — the
|
||||||
|
SHA-256 digest of the peer's DER-encoded leaf certificate.
|
||||||
|
Generate with `openssl x509 -noout -fingerprint -sha256`,
|
||||||
|
then strip the colons and prepend `sha256:`. A malformed
|
||||||
|
value is ignored with a warning rather than weakening
|
||||||
|
trust. See docs/swarm.md for the full recipe.
|
||||||
|
|
||||||
|
Scopes only to hive-c0re's own peer HTTPS checks — it does
|
||||||
|
NOT help Matrix federation (tuwunel validates against its
|
||||||
|
container trust bundle). For a self-signed peer whose root
|
||||||
|
CA you want trusted hive-wide (every agent + Matrix
|
||||||
|
federation), set `caCert` below.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
caCert = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.path;
|
||||||
|
default = null;
|
||||||
|
example = "./peers/edge-ca.pem";
|
||||||
|
description = ''
|
||||||
|
Path to this peer hive's root CA certificate (PEM). When
|
||||||
|
set, the CA is embedded (at build time, into the nix store
|
||||||
|
— no runtime file on the host) and trusted **everywhere the
|
||||||
|
hive's own internal CA is**: it rides alongside `hive-ca.pem`
|
||||||
|
in each agent's `security.pki.certificateFiles` (via the
|
||||||
|
meta-flake renderer), and is added to the Matrix homeserver
|
||||||
|
container's trust bundle so tuwunel validates *federation*
|
||||||
|
TLS from a self-signed peer hive whose cert chains to it.
|
||||||
|
This is the CA-trust path that `certFingerprint`
|
||||||
|
(leaf-pinning, c0re-only) can't cover, and is what unblocks
|
||||||
|
Matrix federation with a self-signed peer hive. Trust stays
|
||||||
|
inside the hive (agents + the Matrix container), never the
|
||||||
|
host system trust store. Mutually complementary with
|
||||||
|
`certFingerprint`; set `caCert` for the federation case. See
|
||||||
|
docs/swarm.md.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
wireguardPublicKey = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "base64pubkey=";
|
||||||
|
description = ''
|
||||||
|
WireGuard public key for this peer host. Required when
|
||||||
|
`services.hyperhive.swarm.wireguard.enable = true` and
|
||||||
|
you want this peer reachable over the mesh. Null = TLS-
|
||||||
|
only peering (public internet, no mesh tunnel).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
wireguardEndpoint = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "203.0.113.1:51820";
|
||||||
|
description = ''
|
||||||
|
WireGuard endpoint for this peer in `host:port` form.
|
||||||
|
Required when the peer host is behind a firewall and
|
||||||
|
this host needs to initiate the tunnel. Null = this host
|
||||||
|
waits for the peer to connect (peer-initiates; peer must
|
||||||
|
have an endpoint pointing back at this host).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
wireguardAddress = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "10.100.0.2/32";
|
||||||
|
description = ''
|
||||||
|
IP address (with prefix) of the peer host on the
|
||||||
|
WireGuard mesh. Used as the `allowedIPs` for the peer's
|
||||||
|
WireGuard config entry and injected into `HYPERHIVE_PEERS`
|
||||||
|
so hive-c0re can route intra-swarm traffic to the mesh
|
||||||
|
address rather than the public domain. Required to include
|
||||||
|
the peer in the WireGuard mesh (peers missing this field
|
||||||
|
are silently excluded from `wg-hive`).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
default = { };
|
||||||
|
example = {
|
||||||
|
"lab.example.com" = {
|
||||||
|
certFingerprint = "sha256:b1946ac92492d2347c6235b4d2611184a3f5b6cae6c19d6e3c2f0a8e7d4c9f12";
|
||||||
|
};
|
||||||
|
"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. Add `wireguardPublicKey` + `wireguardAddress`
|
||||||
|
(and optionally `wireguardEndpoint`) to include the peer in the
|
||||||
|
WireGuard mesh when `swarm.wireguard.enable = true`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# WireGuard mesh config for the local host.
|
||||||
|
# When enabled, a `wg-hive` interface connects to all peers that have
|
||||||
|
# `wireguardPublicKey` declared. Peers reachable over the mesh are
|
||||||
|
# preferred for inter-hive traffic (no public TLS round-trip needed);
|
||||||
|
# peers without a public key still work via normal HTTPS.
|
||||||
|
options.services.hyperhive.swarm.wireguard = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Enable the WireGuard inter-hive mesh. When true, a `wg-hive`
|
||||||
|
interface is brought up connecting to all swarm peers that
|
||||||
|
declare a `wireguardPublicKey`. Requires
|
||||||
|
`privateKeyFile` to be set.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
privateKeyFile = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.path;
|
||||||
|
default = null;
|
||||||
|
example = "/etc/wireguard/hive.key";
|
||||||
|
description = ''
|
||||||
|
Path to the host's WireGuard private key file. The file must
|
||||||
|
be readable by root and should have mode 0400. Generate with
|
||||||
|
`wg genkey > /etc/wireguard/hive.key`. Required when
|
||||||
|
`swarm.wireguard.enable = true`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
address = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "";
|
||||||
|
example = "10.100.0.1/24";
|
||||||
|
description = ''
|
||||||
|
IP address (with prefix) of this host on the WireGuard mesh.
|
||||||
|
Use a /24 (or broader) prefix so the routing table covers all
|
||||||
|
peer /32 routes. Example: `"10.100.0.1/24"` for a 256-host mesh.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
listenPort = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 51820;
|
||||||
|
description = ''
|
||||||
|
UDP port the local WireGuard interface listens on. Must be
|
||||||
|
reachable from peer hosts when they initiate the tunnel.
|
||||||
|
Default: 51820 (standard WireGuard port).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
persistentKeepalive = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.int;
|
||||||
|
default = 25;
|
||||||
|
example = 25;
|
||||||
|
description = ''
|
||||||
|
Seconds between keepalive packets sent to each peer. Useful
|
||||||
|
when this host (or a peer) is behind NAT — keeps the UDP hole
|
||||||
|
open. Set to null to disable. Default: 25 seconds.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Gated on the c0re daemon being enabled (the historical shape — the
|
||||||
|
# mesh is part of the coordinator host's networking).
|
||||||
|
config = lib.mkIf config.services.hyperhive.c0re.enable {
|
||||||
|
assertions = lib.optionals config.services.hyperhive.swarm.wireguard.enable [
|
||||||
|
{
|
||||||
|
assertion = config.services.hyperhive.swarm.wireguard.privateKeyFile != null;
|
||||||
|
message = ''
|
||||||
|
services.hyperhive.swarm.wireguard.enable requires
|
||||||
|
services.hyperhive.swarm.wireguard.privateKeyFile to be set.
|
||||||
|
Generate a key: wg genkey > /etc/wireguard/hive.key
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = config.services.hyperhive.swarm.wireguard.address != "";
|
||||||
|
message = ''
|
||||||
|
services.hyperhive.swarm.wireguard.enable requires
|
||||||
|
services.hyperhive.swarm.wireguard.address to be set
|
||||||
|
(e.g. "10.100.0.1/24").
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
# WireGuard inter-hive mesh. Brings up a `wg-hive` interface and
|
||||||
|
# connects to each peer that has `wireguardPublicKey` set.
|
||||||
|
networking.wireguard.interfaces = lib.mkIf config.services.hyperhive.swarm.wireguard.enable (
|
||||||
|
let
|
||||||
|
wgCfg = config.services.hyperhive.swarm.wireguard;
|
||||||
|
meshPeers = lib.filterAttrs (
|
||||||
|
_: p: p.wireguardPublicKey != null && p.wireguardAddress != null
|
||||||
|
) config.services.hyperhive.swarm.peers;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
wg-hive = {
|
||||||
|
ips = [ wgCfg.address ];
|
||||||
|
listenPort = wgCfg.listenPort;
|
||||||
|
privateKeyFile = wgCfg.privateKeyFile;
|
||||||
|
peers = lib.mapAttrsToList (
|
||||||
|
_domain: p:
|
||||||
|
{
|
||||||
|
publicKey = p.wireguardPublicKey;
|
||||||
|
allowedIPs = [ p.wireguardAddress ];
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (p.wireguardEndpoint != null) {
|
||||||
|
endpoint = p.wireguardEndpoint;
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (wgCfg.persistentKeepalive != null) {
|
||||||
|
persistentKeepalive = wgCfg.persistentKeepalive;
|
||||||
|
}
|
||||||
|
) meshPeers;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
# Open the WireGuard UDP port on the host firewall when the mesh is
|
||||||
|
# on (host-level networking — not inside containers).
|
||||||
|
networking.firewall.allowedUDPPorts = lib.mkIf config.services.hyperhive.swarm.wireguard.enable [
|
||||||
|
config.services.hyperhive.swarm.wireguard.listenPort
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue