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 = {
|
||||
agent-base = ./nix/templates/agent-base.nix;
|
||||
ruth = ./nix/templates/manager.nix;
|
||||
# The hive-c0re module wants `pkgs.hyperhive` for its default
|
||||
# `services.hyperhive.c0re.package`. To avoid making operators apply an
|
||||
# overlay (which would also pollute their host pkgs with our
|
||||
# build), we thread the package straight from this flake's
|
||||
# `packages.<system>.default` via a `hyperhivePackage` argument.
|
||||
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:
|
||||
# The full host stack (nix/modules/default.nix aggregator) plus
|
||||
# the package/source wiring from this flake. The wiring is a
|
||||
# plain config module setting the `services.hyperhive.c0re.*`
|
||||
# package options via `lib.mkDefault` — no overlay involved, and
|
||||
# an operator override still wins. Intended usage:
|
||||
#
|
||||
# imports = [ hyperhive.nixosModules.default ];
|
||||
# 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 =
|
||||
|
|
|
|||
|
|
@ -37,21 +37,14 @@ let
|
|||
# Stub host system: every hyperhive subsystem `mkForce false` so
|
||||
# heavy build inputs (matrix container, forge, etc.) stay out of
|
||||
# the eval — only option *declarations* matter for the doc walk.
|
||||
# Import hive-c0re.nix from the content-addressed nixSrc with stub
|
||||
# package args so the eval doesn't depend on self's Rust builds.
|
||||
# Import the host-module aggregator from the content-addressed
|
||||
# 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 {
|
||||
system = pkgs.stdenv.hostPlatform.system;
|
||||
modules = [
|
||||
(import "${nixSrc}/modules/hive-c0re.nix" {
|
||||
hyperhivePackage = _system: pkgs.emptyFile;
|
||||
hyperhiveFrontend = _system: pkgs.emptyFile;
|
||||
hyperhiveAssets = _system: pkgs.emptyDirectory;
|
||||
hyperhiveFlake = "";
|
||||
hyperhiveDocs = "";
|
||||
hyperhiveXdgIcons = _system: pkgs.emptyFile;
|
||||
agentBaseToplevel = pkgs.emptyFile;
|
||||
managerToplevel = pkgs.emptyFile;
|
||||
})
|
||||
"${nixSrc}/modules"
|
||||
(
|
||||
{ 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 @@
|
|||
{
|
||||
hyperhivePackage,
|
||||
hyperhiveFrontend,
|
||||
hyperhiveAssets,
|
||||
hyperhiveFlake,
|
||||
hyperhiveDocs,
|
||||
hyperhiveXdgIcons,
|
||||
agentBaseToplevel,
|
||||
managerToplevel,
|
||||
}:
|
||||
# The hive-c0re coordinator daemon (unprivileged `hive-core` user) and
|
||||
# its narrow root helper hive-priv, both socket-activated. Options
|
||||
# under `services.hyperhive.c0re.*`. The package/source options
|
||||
# (`package`, `frontend`, `assets`, `xdgIcons`, `hyperhiveFlake`,
|
||||
# `hyperhiveDocs`, `agentBaseToplevel`, `managerToplevel`) have no
|
||||
# in-module defaults — the flake's `nixosModules.default` wires them
|
||||
# to this flake's own package outputs via `lib.mkDefault`, so
|
||||
# operator overrides still win and no overlay is involved.
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
|
|
@ -114,404 +112,6 @@ let
|
|||
if stylixThemeColors != null then themedFrontend stylixThemeColors else cfg.frontend;
|
||||
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 = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
|
|
@ -521,21 +121,22 @@ in
|
|||
};
|
||||
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).
|
||||
(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 {
|
||||
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
|
||||
Bundled frontend dist (see `nix/packages/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
|
||||
|
|
@ -551,16 +152,15 @@ in
|
|||
description = ''
|
||||
Internal, read-only: `frontend` re-themed with the active stylix
|
||||
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.
|
||||
'';
|
||||
};
|
||||
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
|
||||
Bundled static runtime assets (see `nix/packages/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`
|
||||
|
|
@ -569,21 +169,30 @@ in
|
|||
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 {
|
||||
type = lib.types.str;
|
||||
default = hyperhiveFlake;
|
||||
defaultText = lib.literalMD "the flake's own store path";
|
||||
defaultText = lib.literalMD "the hyperhive flake's own filtered source 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.
|
||||
build the container. Wired by `nixosModules.default` to this
|
||||
flake's own filtered source — only override if you want agents
|
||||
tracking a different ref.
|
||||
'';
|
||||
};
|
||||
hyperhiveDocs = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = hyperhiveDocs;
|
||||
defaultText = lib.literalMD "the docs/ tree's own store path";
|
||||
description = ''
|
||||
URL of the narrow `docs/` source (no fragment). Inlined into the
|
||||
|
|
@ -594,6 +203,24 @@ in
|
|||
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 {
|
||||
type = lib.types.str;
|
||||
default = "path:${pkgs.path}";
|
||||
|
|
@ -657,8 +284,8 @@ in
|
|||
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.
|
||||
`nix build .#agent-base-toplevel` once manually to warm the
|
||||
store.
|
||||
'';
|
||||
};
|
||||
contextWindowTokens = lib.mkOption {
|
||||
|
|
@ -769,9 +396,8 @@ in
|
|||
systemd `CPUQuota=` applied to every agent container via a
|
||||
`container@h-<name>.service.d/` drop-in written on each
|
||||
spawn/rebuild. Expressed as a percentage of one CPU core —
|
||||
`"200%"` allows each agent to use up to 2 cores. The old
|
||||
hard-coded value was `"50%"`; bump this if agents are hitting
|
||||
CPU limits during builds or heavy tool use.
|
||||
`"200%"` allows each agent to use up to 2 cores. Bump this if
|
||||
agents are hitting CPU limits during builds or heavy tool use.
|
||||
|
||||
For a hive-wide cap across all containers, set
|
||||
`systemd.slices.machine.serviceConfig.CPUQuota` in your NixOS
|
||||
|
|
@ -785,8 +411,7 @@ in
|
|||
example = "8G";
|
||||
description = ''
|
||||
systemd `MemoryMax=` applied to every agent container via the
|
||||
same drop-in as `agentCpuQuota`. The old hard-coded value was
|
||||
`"2G"`.
|
||||
same drop-in as `agentCpuQuota`.
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
@ -798,11 +423,10 @@ in
|
|||
Number of nix-heavy job-queue nodes (container prebuilds,
|
||||
profile swaps, first-spawn creates, meta lock bumps) hive-c0re
|
||||
runs concurrently. The default of 1 serializes all heavy nix
|
||||
work like the pre-DAG rebuild queue did; raise it on hosts with
|
||||
the cores/RAM to build several agent toplevels at once.
|
||||
Per-agent correctness is independent of this count — each
|
||||
agent's container-affecting operations are serialized by its
|
||||
lifecycle lease regardless.
|
||||
work; raise it on hosts with the cores/RAM to build several
|
||||
agent toplevels at once. Per-agent correctness is independent
|
||||
of this count — each agent's container-affecting operations are
|
||||
serialized by its lifecycle lease regardless.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
|
@ -813,7 +437,7 @@ in
|
|||
pkgs.git
|
||||
# XDG icons + .desktop entries so desktop environments can match
|
||||
# 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
|
||||
|
|
@ -828,8 +452,8 @@ in
|
|||
# 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
|
||||
cfg.agentBaseToplevel
|
||||
cfg.managerToplevel
|
||||
];
|
||||
|
||||
# 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
|
||||
# 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
|
||||
# enabled — the canonical assertion lives in `hive-network.nix` (the
|
||||
# hive resolver is authoritative for `<domain>` and agents reach the
|
||||
# forge/matrix through the gateway by it). So everything below can
|
||||
# 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 = {
|
||||
description = "hyperhive coordinator daemon";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
|
@ -941,7 +500,7 @@ in
|
|||
HOME = "/var/lib/hyperhive";
|
||||
HYPERHIVE_GIT = "${pkgs.git}/bin/git";
|
||||
# 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
|
||||
# gateway_nginx.rs uses this to generate split location
|
||||
# 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.
|
||||
HIVE_ASSETS_DIR = "${cfg.assets}/share/hyperhive";
|
||||
# Whether this hive runs ruthless — no root/manager agent at all
|
||||
# (`auto_update::ensure_root_agent`). Default false = historical
|
||||
# behaviour (root auto-managed); true makes the sweep a no-op.
|
||||
# (`auto_update::ensure_root_agent`). Default false = root
|
||||
# auto-managed; true makes the sweep a no-op.
|
||||
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