refactor: nix/host-modules + nix/agent-modules layout, update doc paths
This commit is contained in:
parent
cb755b677c
commit
4a48ce5024
52 changed files with 48 additions and 44 deletions
218
nix/host-modules/hive-c0re/default.nix
Normal file
218
nix/host-modules/hive-c0re/default.nix
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
# The hive-c0re coordinator daemon (runs as the unprivileged
|
||||
# `hive-core` user), socket-activated at /run/hyperhive/host.sock.
|
||||
# Layout: ./options.nix (option declarations), ./theme.nix (stylix
|
||||
# frontend theming → `servedFrontend`), ./environment.nix (the daemon
|
||||
# unit's env attrset). The root privileged helper it delegates to is
|
||||
# its own module (../hive-priv.nix).
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.services.hyperhive.c0re;
|
||||
|
||||
# Privsep splits ownership across users, so git/libgit2's dubious-
|
||||
# ownership guard trips on legitimate cross-user reads: hive-priv (root)
|
||||
# fetches the hive-core-owned meta/applied repos via nix, and hive-c0re
|
||||
# (hive-core) fetches the agent-owned proposed-config repos. Both
|
||||
# processes are trusted and can already read the files; this gitconfig
|
||||
# only satisfies the ownership guard. libgit2 honours the literal `*`
|
||||
# (mid-path globs aren't supported, so per-agent repos can't be listed);
|
||||
# in practice these processes only ever touch hyperhive's own repos.
|
||||
safeDirGitconfig = pkgs.writeText "hyperhive-safe-gitconfig" ''
|
||||
[safe]
|
||||
directory = *
|
||||
'';
|
||||
|
||||
# The `hive-c0re serve` config JSON. Keys are snake_case to match the
|
||||
# `ServeConfig` serde shape the daemon deserialises (the
|
||||
# container-injected HiveEnv fields, flattened, plus the hive-c0re-local
|
||||
# model_prices table); per-flag overrides still work for ad-hoc
|
||||
# invocations.
|
||||
#
|
||||
# Written to `/etc/hyperhive/serve.json` (managed by
|
||||
# `environment.etc`) rather than embedded as a store-path argument in
|
||||
# ExecStart. This keeps ExecStart byte-stable across deploys that only
|
||||
# change hyperhive module files (gateway, frontend, unrelated nix
|
||||
# modules) so systemd does NOT restart hive-c0re — and therefore does
|
||||
# NOT trigger a startup sweep that rebuilds every agent — unless the
|
||||
# c0re binary itself changes.
|
||||
serveConfigJson = builtins.toJSON {
|
||||
hyperhive_flake = cfg.hyperhiveFlake;
|
||||
hyperhive_docs_flake = cfg.hyperhiveDocs;
|
||||
nixpkgs_flake = cfg.nixpkgsFlake;
|
||||
dashboard_port = cfg.dashboardPort;
|
||||
operator_pronouns = cfg.operatorPronouns;
|
||||
context_window_tokens = cfg.contextWindowTokens;
|
||||
agent_cpu_quota = cfg.agentCpuQuota;
|
||||
agent_memory_max = cfg.agentMemoryMax;
|
||||
model_prices = cfg.modelPrices;
|
||||
build_slots = cfg.buildSlots;
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
./options.nix
|
||||
./theme.nix
|
||||
];
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
environment.systemPackages = [
|
||||
cfg.package
|
||||
pkgs.git
|
||||
# XDG icons + .desktop entries so desktop environments can match
|
||||
# hyperhive processes to their icon (task managers, CPU monitors, etc.).
|
||||
cfg.xdgIcons
|
||||
];
|
||||
|
||||
# Serve config at a stable /etc path so hive-c0re's ExecStart
|
||||
# doesn't embed a volatile store-path argument. See serveConfigJson
|
||||
# above for the rationale.
|
||||
environment.etc."hyperhive/serve.json".text = serveConfigJson;
|
||||
|
||||
# Pull the per-container toplevels into the host system closure.
|
||||
# `system.extraDependencies` adds paths to the system build
|
||||
# without referencing them at runtime — nixos-rebuild fetches /
|
||||
# builds them, they end up in /nix/store, and the first
|
||||
# nixos-container update + start for an agent has nothing left to
|
||||
# do. Gated because the closure is sizeable and pinned to x86_64.
|
||||
system.extraDependencies = lib.optionals cfg.preBuildAgentTemplates [
|
||||
cfg.agentBaseToplevel
|
||||
cfg.managerToplevel
|
||||
];
|
||||
|
||||
# Unprivileged coordinator user. hive-c0re runs as this user;
|
||||
# privileged operations are delegated to hive-priv which runs as
|
||||
# root, socket-activated at /run/hive/priv.sock (./hive-priv.nix).
|
||||
users.users.hive-core = {
|
||||
isSystemUser = true;
|
||||
group = "hive-core";
|
||||
description = "hive-c0re coordinator daemon user";
|
||||
};
|
||||
users.groups.hive-core = { };
|
||||
|
||||
# The gateway nginx is always the sole external entry point (it runs
|
||||
# alongside hyperhive), so the per-agent web-port range stays closed on
|
||||
# the host firewall. See `docs/gateway.md::Firewall posture (host-level)`.
|
||||
|
||||
# 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 the daemon environment
|
||||
# (./environment.nix) can treat it as non-null.
|
||||
systemd.services.hive-c0re = {
|
||||
description = "hyperhive coordinator daemon";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
# Socket unit must start before the service so hive-c0re receives the
|
||||
# pre-bound fd via LISTEN_FDS (socket activation). Without this
|
||||
# dependency, nixos-rebuild switch activates hive-c0re.socket while
|
||||
# hive-c0re.service is already running (started by multi-user.target),
|
||||
# and systemd refuses with "Socket service already active". Adding
|
||||
# requires+after causes systemd to stop the service, start the socket,
|
||||
# then restart the service -- clean transition on every config apply.
|
||||
requires = [ "hive-c0re.socket" ];
|
||||
after = [ "hive-c0re.socket" ];
|
||||
path = [
|
||||
pkgs.git
|
||||
"/run/current-system/sw"
|
||||
];
|
||||
environment = import ./environment.nix { inherit lib config pkgs; };
|
||||
serviceConfig = {
|
||||
ExecStart = "${cfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --config /etc/hyperhive/serve.json";
|
||||
SyslogIdentifier = "hive-c0re";
|
||||
# Migrate hive-c0re's *own* state to the service user after an
|
||||
# upgrade from a root-run install (systemd's StateDirectory only
|
||||
# chowns the top-level dir, not pre-existing files inside it). The
|
||||
# `+` prefix runs as root despite User = hive-core; `-` tolerates
|
||||
# failure. coreutils ships `chown` but no `sh`, so invoke the
|
||||
# binaries directly rather than through a shell.
|
||||
#
|
||||
# CRITICAL: exclude the per-agent `agents/` subtree. Its contents
|
||||
# (each agent's `claude/` OAuth creds, `state/`, `harness/`,
|
||||
# `config/`) are owned by the per-agent / manager users, and each
|
||||
# container's `hive-agent-user-migrate` activation script chowns
|
||||
# them back to that user on boot. Blanket-chowning them to hive-core
|
||||
# makes every agent's `~/.claude` unreadable — logging them all out
|
||||
# with no way to log back in. So chown everything *except* agents/,
|
||||
# plus the `agents/` dir node itself (not its contents) so c0re can
|
||||
# still create new per-agent subdirs.
|
||||
ExecStartPre = [
|
||||
# Install the safe.directory gitconfig at $HOME/.gitconfig
|
||||
# (HOME = /var/lib/hyperhive) so c0re's `git fetch`/`rev-parse`
|
||||
# against the agent-owned proposed repos pass the ownership guard.
|
||||
# Placed before the chown below so it's chowned to hive-core too.
|
||||
"+-${pkgs.coreutils}/bin/cp ${safeDirGitconfig} /var/lib/hyperhive/.gitconfig"
|
||||
"+-${pkgs.findutils}/bin/find /var/lib/hyperhive -mindepth 1 -maxdepth 1 -not -name agents -exec ${pkgs.coreutils}/bin/chown -R hive-core:hive-core {} +"
|
||||
"+-${pkgs.coreutils}/bin/chown hive-core:hive-core /var/lib/hyperhive/agents"
|
||||
];
|
||||
Restart = "on-failure";
|
||||
RestartSec = 2;
|
||||
User = "hive-core";
|
||||
Group = "hive-core";
|
||||
SupplementaryGroups = [ "systemd-journal" ];
|
||||
RuntimeDirectory = "hyperhive";
|
||||
RuntimeDirectoryMode = "0750";
|
||||
RuntimeDirectoryPreserve = "yes";
|
||||
StateDirectory = "hyperhive";
|
||||
StateDirectoryMode = "0750";
|
||||
# Sandboxing. hive-c0re is unprivileged (runs as hive-core, never
|
||||
# setuid), makes HTTP requests to forge/matrix/Anthropic (keeps INET),
|
||||
# and delegates all privileged ops to hive-priv via a Unix socket.
|
||||
# These directives deny the subset of kernel capabilities it
|
||||
# provably doesn't need without restricting its network or
|
||||
# filesystem access (RestrictAddressFamilies deferred — needs a
|
||||
# watched deploy to verify no AF_UNIX/AF_INET gaps in socket paths).
|
||||
NoNewPrivileges = true; # already runs as unprivileged user
|
||||
PrivateTmp = true; # uses StateDirectory for tmpfiles, not /tmp
|
||||
ProtectHome = true; # HOME = /var/lib/hyperhive; no /home/* access needed
|
||||
# "strict" makes the entire filesystem read-only except for
|
||||
# StateDirectory (/var/lib/hyperhive) and RuntimeDirectory
|
||||
# (/run/hyperhive), which systemd keeps writable. No
|
||||
# ReadWritePaths needed beyond the managed directories because:
|
||||
# - nix is invoked directly (lifecycle, meta, flake_check), but
|
||||
# NIX_REMOTE=daemon routes all store writes through the host
|
||||
# daemon — hive-c0re never writes to /nix itself.
|
||||
# - flake.lock ops land in the meta worktree under StateDirectory
|
||||
# (kept writable by systemd).
|
||||
# - nix build worktrees live in PrivateTmp, not /tmp.
|
||||
# - /etc writes (bind-mount edits) go through hive-priv via the
|
||||
# privileged socket; /etc/hyperhive/serve.json is read-only.
|
||||
ProtectSystem = "strict";
|
||||
ProtectKernelTunables = true; # no sysctl writes
|
||||
ProtectKernelLogs = true; # reads logs via systemd-journal group, not /dev/kmsg
|
||||
ProtectControlGroups = true; # cgroup writes go through hive-priv, not c0re directly
|
||||
RestrictNamespaces = true; # namespace creation goes through hive-priv
|
||||
LockPersonality = true; # no personality changes needed
|
||||
RestrictRealtime = true; # no real-time scheduling
|
||||
};
|
||||
};
|
||||
|
||||
# Socket unit for the hive-c0re admin socket. systemd creates and holds
|
||||
# `/run/hyperhive/host.sock` before hive-c0re starts, then passes the fd
|
||||
# via LISTEN_FDS (socket activation). Benefits: `hivectl` can connect
|
||||
# the moment the socket unit is active — no racy retry window — and a
|
||||
# hive-c0re restart never drops the socket inode, so queued commands
|
||||
# drain cleanly.
|
||||
#
|
||||
# `hive-c0re serve` reads LISTEN_FDS via the `listenfd` crate and
|
||||
# accepts the fd in preference to its own `bind()` path. When invoked
|
||||
# directly (dev, CI, without the socket unit) LISTEN_FDS is absent and
|
||||
# the traditional bind path runs unchanged — no regression.
|
||||
systemd.sockets.hive-c0re = {
|
||||
description = "hive-c0re admin socket";
|
||||
wantedBy = [ "sockets.target" ];
|
||||
socketConfig = {
|
||||
# Must match the `--socket` arg passed to `hive-c0re serve`.
|
||||
ListenStream = "/run/hyperhive/host.sock";
|
||||
# 0660 root:root — `hivectl` is a host-only tool run as root.
|
||||
SocketMode = "0660";
|
||||
# Parent dir inherits the RuntimeDirectory mode (0750) set on the
|
||||
# service unit; DirectoryMode is only consulted when the dir is
|
||||
# absent at socket-unit activation.
|
||||
DirectoryMode = "0750";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
180
nix/host-modules/hive-c0re/environment.nix
Normal file
180
nix/host-modules/hive-c0re/environment.nix
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
# Environment of the hive-c0re daemon unit — a plain function file
|
||||
# (not a module) returning the env attrset, imported by ./default.nix.
|
||||
# Everything meta.rs forwards into agent containers or reads for the
|
||||
# meta-flake render is assembled here.
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
}:
|
||||
let
|
||||
cfg = config.services.hyperhive.c0re;
|
||||
in
|
||||
{
|
||||
# nix (the prebuild `nix build`, flake-check, and meta eval) writes
|
||||
# its fetcher/eval cache under $HOME/.cache. As a system user
|
||||
# hive-core has no home, so HOME defaults to the unwritable
|
||||
# /var/empty and Lix fails to initialise its cache. Point HOME at
|
||||
# the writable StateDirectory.
|
||||
HOME = "/var/lib/hyperhive";
|
||||
HYPERHIVE_GIT = "${pkgs.git}/bin/git";
|
||||
# No HIVE_STATIC_DIR: the gateway static-serves the dashboard dist
|
||||
# (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
|
||||
# nix store directly; dynamic API paths still proxied to the
|
||||
# agent daemon. The nix store is shared across nspawn
|
||||
# containers, so this path is reachable from inside the
|
||||
# gateway container's nginx.
|
||||
HIVE_AGENT_FRONTEND_DIR = "${cfg.servedFrontend}/agent";
|
||||
# Path to the static runtime asset tree (branding + claude
|
||||
# prompts). `hive_sh4re::assets::*` reads paths underneath.
|
||||
# `forge.rs` reads the avatar PNGs from here on startup.
|
||||
HIVE_ASSETS_DIR = "${cfg.assets}/share/hyperhive";
|
||||
# Whether this hive runs ruthless — no root/manager agent at all
|
||||
# (`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;
|
||||
}
|
||||
// {
|
||||
# Identity env vars threaded into c0re's own service env and
|
||||
# forwarded by meta.rs into every sub-agent's harness env —
|
||||
# full chain in docs/conventions.md::Hive identity. `domain` is
|
||||
# required (asserted in hive-network.nix), so it's always set.
|
||||
HYPERHIVE_HIVE_DOMAIN = config.services.hyperhive.domain;
|
||||
}
|
||||
// lib.optionalAttrs (config.services.hyperhive.hiveName != null) {
|
||||
HYPERHIVE_HIVE_NAME = config.services.hyperhive.hiveName;
|
||||
}
|
||||
// lib.optionalAttrs (config.services.hyperhive.swarmName != null) {
|
||||
HYPERHIVE_SWARM_NAME = config.services.hyperhive.swarmName;
|
||||
}
|
||||
// lib.optionalAttrs (!config.services.hyperhive.github.enable) {
|
||||
# GitHub integration is on by default; only signal the OFF override to
|
||||
# meta.rs, which then injects `hyperhive.github.enable = false` into
|
||||
# every agent. See services.hyperhive.github.enable.
|
||||
HYPERHIVE_GITHUB_DISABLED = "1";
|
||||
}
|
||||
// lib.optionalAttrs config.services.hyperhive.otel.enable (
|
||||
# Hive-wide OTEL config -> read by meta.rs::otel_config and
|
||||
# injected as build-time `hyperhive.otel.*` into every agent.
|
||||
# Endpoint presence is the enable signal on the meta side; the
|
||||
# optional fields are only emitted when set so absent values
|
||||
# don't render no-op env lines.
|
||||
let
|
||||
otel = config.services.hyperhive.otel;
|
||||
in
|
||||
{
|
||||
HYPERHIVE_OTEL_ENDPOINT = otel.endpoint;
|
||||
HYPERHIVE_OTEL_PROTOCOL = otel.protocol;
|
||||
}
|
||||
// lib.optionalAttrs (otel.extraResourceAttributes != "") {
|
||||
HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES = otel.extraResourceAttributes;
|
||||
}
|
||||
// lib.optionalAttrs (otel.headersCredential != null) {
|
||||
HYPERHIVE_OTEL_HEADERS_CREDENTIAL = otel.headersCredential;
|
||||
}
|
||||
// lib.optionalAttrs (otel.metricIntervalMs != null) {
|
||||
HYPERHIVE_OTEL_METRIC_INTERVAL_MS = toString otel.metricIntervalMs;
|
||||
}
|
||||
// lib.optionalAttrs otel.debug {
|
||||
HYPERHIVE_OTEL_DEBUG = "1";
|
||||
}
|
||||
)
|
||||
// {
|
||||
# In-cluster forge URL — the gateway vhost (`forge.<domain>`), which
|
||||
# nginx proxies to forgejo. Used both for internal API calls in
|
||||
# hive-c0re (forge/mod.rs `forge_http_base()`) and forwarded to
|
||||
# agents via meta.rs for their forge-notify client. The forge is
|
||||
# mandatory, so this is unconditional (the whole env block is already
|
||||
# gated on hyperhive being enabled). See `docs/gateway.md::HIVE_FORGE_URL`.
|
||||
HIVE_FORGE_URL = "http://${config.services.hyperhive.forge.domain}";
|
||||
}
|
||||
// lib.optionalAttrs config.services.hyperhive.matrix.enable {
|
||||
# In-cluster matrix homeserver URL for each agent's
|
||||
# hive-matrix-daemon — the gateway vhost (`matrix.<domain>`). The
|
||||
# gatewayHost null-guard falls back to loopback so a domain-less
|
||||
# config still evals. Forwarded to agents by meta.rs alongside
|
||||
# HIVE_FORGE_URL; shares the same env-forwarding ordering caveat
|
||||
# (value baked at config-generation time).
|
||||
HIVE_MATRIX_URL =
|
||||
if config.services.hyperhive.matrix.gatewayHost != null then
|
||||
"http://${config.services.hyperhive.matrix.gatewayHost}"
|
||||
else
|
||||
"http://127.0.0.1:${toString config.services.hyperhive.matrix.httpPort}";
|
||||
}
|
||||
// lib.optionalAttrs config.services.hyperhive.matrix.gui.enable {
|
||||
# Availability flags read by the dashboard's `/api/state`.
|
||||
# Matrix GUI lives entirely on the gateway nginx (matrix tab
|
||||
# only shows when both flags are on). Gateway routing detail:
|
||||
# docs/gateway.md::Vhost map.
|
||||
HIVE_MATRIX_GUI_ENABLED = "1";
|
||||
}
|
||||
// {
|
||||
# The gateway always runs, so the dashboard always builds
|
||||
# same-origin `/agent/<name>/` links (never the direct
|
||||
# `<host>:<port>` TCP fallback). Kept as an env flag so the
|
||||
# dashboard doesn't need to learn the gateway is unconditional.
|
||||
HIVE_GATEWAY_ENABLED = "1";
|
||||
}
|
||||
// lib.optionalAttrs config.services.hyperhive.forge.behindGateway {
|
||||
# Public URL of the forge vhost served by hive-gateway. The
|
||||
# dashboard uses this to build browser-facing forge links
|
||||
# instead of hardcoding `<hostname>:3000`, which breaks when
|
||||
# the operator accesses the dashboard through the gateway
|
||||
# (forge sub-domain has no port; direct port URL would be
|
||||
# wrong). Absent when `behindGateway = false` — dashboard
|
||||
# falls back to `<hostname>:3000`.
|
||||
HIVE_FORGE_PUBLIC_URL = "https://${config.services.hyperhive.forge.domain}";
|
||||
}
|
||||
//
|
||||
lib.optionalAttrs
|
||||
(
|
||||
config.services.hyperhive.matrix.gui.enable && config.services.hyperhive.matrix.gatewayHost != null
|
||||
)
|
||||
{
|
||||
# Browser-facing matrix GUI (fluffychat) URL — the gateway
|
||||
# vhost (`matrix.<domain>`). Surfaced via the daemon's `Urls`
|
||||
# request for `hivectl open matrix`. Absent when the GUI is off
|
||||
# or no gatewayHost is set (no browser-reachable matrix vhost).
|
||||
HIVE_MATRIX_PUBLIC_URL = "https://${config.services.hyperhive.matrix.gatewayHost}/";
|
||||
}
|
||||
// lib.optionalAttrs (config.services.hyperhive.swarm.peers != { }) {
|
||||
# Peer hives serialised as a JSON array of {domain, cert_fingerprint,
|
||||
# wireguard_address?} objects. Consumed by hive-ag3nt::identity::peers()
|
||||
# + the dashboard's peer_hives StateSnapshot field (P33RS tab). Domain
|
||||
# is the attrset key; cert_fingerprint is null for CA-trusted peers;
|
||||
# wireguard_address is omitted when not part of the mesh.
|
||||
HYPERHIVE_PEERS = builtins.toJSON (
|
||||
lib.mapAttrsToList (
|
||||
domain: p:
|
||||
{
|
||||
inherit domain;
|
||||
cert_fingerprint = p.certFingerprint;
|
||||
}
|
||||
// lib.optionalAttrs (p.wireguardAddress != null) {
|
||||
wireguard_address = p.wireguardAddress;
|
||||
}
|
||||
) config.services.hyperhive.swarm.peers
|
||||
);
|
||||
}
|
||||
//
|
||||
lib.optionalAttrs
|
||||
(lib.any (p: p.caCert != null) (lib.attrValues config.services.hyperhive.swarm.peers))
|
||||
{
|
||||
# Peer-hive root CA file paths (colon-joined), one per peer that
|
||||
# declares `swarm.peers.<domain>.caCert`. hive-c0re's meta-flake
|
||||
# renderer (meta.rs) embeds each next to every agent's flake and
|
||||
# adds it to `security.pki.certificateFiles`, so a peer CA is
|
||||
# trusted everywhere the hive's own internal CA (`hive-ca.pem`)
|
||||
# is — i.e. by every agent. The matrix container trusts the same
|
||||
# CAs separately for federation TLS. The `caCert` files are
|
||||
# copied into the nix store at build, so these are store paths —
|
||||
# nothing mutable lives on the host.
|
||||
HIVE_PEER_CA_PATHS = lib.concatStringsSep ":" (
|
||||
lib.filter (c: c != null) (
|
||||
lib.mapAttrsToList (_domain: p: p.caCert) config.services.hyperhive.swarm.peers
|
||||
)
|
||||
);
|
||||
}
|
||||
319
nix/host-modules/hive-c0re/options.nix
Normal file
319
nix/host-modules/hive-c0re/options.nix
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
# Option declarations for `services.hyperhive.c0re.*` — the c0re
|
||||
# daemon's knobs plus the package/source options the flake's
|
||||
# `nixosModules.default` wires to its own outputs (they carry no
|
||||
# in-module defaults; see ../../../flake.nix). The read-only
|
||||
# `servedFrontend` option lives in ./theme.nix with the stylix wiring
|
||||
# that computes it.
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
{
|
||||
options.services.hyperhive.c0re = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = config.services.hyperhive.enable;
|
||||
defaultText = lib.literalExpression "config.services.hyperhive.enable";
|
||||
description = "Enable hive-c0re coordinator daemon (auto-enabled by services.hyperhive.enable).";
|
||||
};
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
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). 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;
|
||||
defaultText = lib.literalExpression "hyperhive.packages.\${system}.frontend";
|
||||
description = ''
|
||||
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
|
||||
endpoints) is the source of truth for any replacement.
|
||||
'';
|
||||
};
|
||||
assets = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
defaultText = lib.literalExpression "hyperhive.packages.\${system}.assets";
|
||||
description = ''
|
||||
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`
|
||||
(`hive_sh4re::assets::*` resolve paths underneath). Override to
|
||||
ship customised branding or prompts without rebuilding the
|
||||
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;
|
||||
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. 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;
|
||||
defaultText = lib.literalMD "the docs/ tree's own store path";
|
||||
description = ''
|
||||
URL of the narrow `docs/` source (no fragment). Inlined into the
|
||||
generated meta `flake.nix` at `inputs.hyperhive-docs.url` and
|
||||
threaded to each agent as `hyperhive.docs.source`, from which the
|
||||
harness resolves `$HIVE_DOCS_DIR`. Its own store path — separate
|
||||
from `hyperhiveFlake` — so a doc edit only re-locks this input
|
||||
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}";
|
||||
defaultText = lib.literalMD "`\"path:\${pkgs.path}\"`";
|
||||
description = ''
|
||||
Store-path URL for the `nixpkgs` input in the generated meta
|
||||
flake. The meta flake declares this as a top-level input and
|
||||
wires `inputs.hyperhive.inputs.nixpkgs.follows = "nixpkgs"` so
|
||||
every agent container evaluates with this exact nixpkgs.
|
||||
|
||||
Defaults to `"path:''${pkgs.path}"` — the store path of the
|
||||
nixpkgs the host NixOS module was evaluated with. When the
|
||||
operator sets `inputs.hyperhive.inputs.nixpkgs.follows =
|
||||
"nixpkgs"` in their host flake, `pkgs.path` resolves to the
|
||||
host's own nixpkgs, so agents transparently track the same
|
||||
channel as the host.
|
||||
|
||||
Override to pin agents to a specific nixpkgs version regardless
|
||||
of the host's channel.
|
||||
'';
|
||||
};
|
||||
dashboardPort = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 7000;
|
||||
description = "TCP port the hive-c0re dashboard listens on.";
|
||||
};
|
||||
operatorPronouns = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "she/her";
|
||||
example = "they/them";
|
||||
description = ''
|
||||
Operator pronouns, free text. Threaded into every agent
|
||||
container as the `HIVE_OPERATOR_PRONOUNS` env var; the
|
||||
harness substitutes it into the agent / manager system
|
||||
prompt at boot so claude refers to the operator naturally
|
||||
in third person ("ask her", "tell them", etc.). Changes
|
||||
propagate to running agents on the next `↻ R3BU1LD` —
|
||||
forwards as a meta flake env-var bump, no per-agent
|
||||
approval needed.
|
||||
'';
|
||||
};
|
||||
preBuildAgentTemplates = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
example = true;
|
||||
description = ''
|
||||
Pre-fetch the per-container system closures (agent-base +
|
||||
manager toplevels) into the host's /nix/store as part of this
|
||||
host's NixOS build, instead of letting the first agent spawn
|
||||
do all the work.
|
||||
|
||||
Enabling this adds roughly the full nixpkgs runtime closure +
|
||||
claude-code + the harness binary to your system closure size
|
||||
(low single-digit GB), but the first `nixos-container start`
|
||||
for any agent then completes in seconds instead of minutes
|
||||
because nothing's left to fetch.
|
||||
|
||||
Off by default because the toplevels are pinned to
|
||||
`x86_64-linux` (nixos-containers run native arch). Enabling
|
||||
on an aarch64 host would force nix to build the x86 closure
|
||||
via cross or a remote builder, which is rarely what you want.
|
||||
Flip to `true` on an x86_64 host when you care more about
|
||||
first-spawn latency than host store size — or just
|
||||
`nix build .#agent-base-toplevel` once manually to warm the
|
||||
store.
|
||||
'';
|
||||
};
|
||||
contextWindowTokens = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.int;
|
||||
default = {
|
||||
haiku = 200000;
|
||||
sonnet = 1000000;
|
||||
opus = 1000000;
|
||||
};
|
||||
example = {
|
||||
haiku = 150000;
|
||||
sonnet = 900000;
|
||||
};
|
||||
description = ''
|
||||
Per-model context-window sizes in tokens. Each key is a
|
||||
model-family short name matched case-insensitively as a
|
||||
substring of the active model name at runtime (e.g. `"sonnet"`
|
||||
matches `"claude-sonnet-4-5"`). The defaults cover the known
|
||||
Anthropic families; add entries for new models or override
|
||||
existing ones here to change the window for all agents at once.
|
||||
|
||||
Passed to `hive-c0re serve` as JSON and injected into every
|
||||
container's harness service environment as
|
||||
`HIVE_CONTEXT_WINDOW_TOKENS_<KEY_UPPER>`. Changes propagate
|
||||
on the next `↻ R3BU1LD` — no per-agent approval needed.
|
||||
'';
|
||||
};
|
||||
|
||||
modelPrices = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
input = lib.mkOption {
|
||||
type = lib.types.numbers.nonnegative;
|
||||
description = "USD per million input tokens.";
|
||||
};
|
||||
output = lib.mkOption {
|
||||
type = lib.types.numbers.nonnegative;
|
||||
description = "USD per million output tokens.";
|
||||
};
|
||||
cache_read = lib.mkOption {
|
||||
type = lib.types.numbers.nonnegative;
|
||||
description = "USD per million cache-read tokens.";
|
||||
};
|
||||
cache_write = lib.mkOption {
|
||||
type = lib.types.numbers.nonnegative;
|
||||
description = "USD per million cache-creation (write) tokens.";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
# Current Anthropic list prices for the Claude 4.x family (Opus
|
||||
# 4.x, Sonnet 4.x, Haiku 4.5); cache_write is the 1-hour cache-TTL
|
||||
# price (the default through the Claude subscription the agents run
|
||||
# on). Keep in sync with `builtin_prices` in
|
||||
# hive-c0re/src/hive_stats.rs.
|
||||
default = {
|
||||
opus = {
|
||||
input = 5.0;
|
||||
output = 25.0;
|
||||
cache_read = 0.5;
|
||||
cache_write = 10.0;
|
||||
};
|
||||
sonnet = {
|
||||
input = 3.0;
|
||||
output = 15.0;
|
||||
cache_read = 0.3;
|
||||
cache_write = 6.0;
|
||||
};
|
||||
haiku = {
|
||||
input = 1.0;
|
||||
output = 5.0;
|
||||
cache_read = 0.1;
|
||||
cache_write = 2.0;
|
||||
};
|
||||
};
|
||||
example = {
|
||||
sonnet = {
|
||||
input = 3.0;
|
||||
output = 15.0;
|
||||
cache_read = 0.3;
|
||||
cache_write = 6.0;
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
Per-model USD prices (per **million** tokens) used for the
|
||||
hive-wide cost *estimate* on the dashboard's ST4TS tab. Each key
|
||||
is a model-family short name matched case-insensitively as a
|
||||
substring of the active model id at runtime (e.g. `"sonnet"`
|
||||
matches `"claude-sonnet-4-5"`); the longest matching key wins, so
|
||||
a specific entry beats a generic family name. Any model not
|
||||
covered by this table falls back to hive-c0re's built-in
|
||||
estimate.
|
||||
|
||||
The defaults track Anthropic list pricing at the time of
|
||||
writing — override them here to keep the estimate current
|
||||
without a code change. Passed to `hive-c0re serve` as JSON via
|
||||
`--model-prices`; read only by hive-c0re itself (not injected
|
||||
into containers). Changes apply on the next host rebuild.
|
||||
'';
|
||||
};
|
||||
|
||||
agentCpuQuota = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "200%";
|
||||
example = "400%";
|
||||
description = ''
|
||||
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. 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
|
||||
config (all nspawn containers live in `machine.slice`).
|
||||
'';
|
||||
};
|
||||
|
||||
agentMemoryMax = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "4G";
|
||||
example = "8G";
|
||||
description = ''
|
||||
systemd `MemoryMax=` applied to every agent container via the
|
||||
same drop-in as `agentCpuQuota`.
|
||||
'';
|
||||
};
|
||||
|
||||
buildSlots = lib.mkOption {
|
||||
type = lib.types.ints.positive;
|
||||
default = 1;
|
||||
example = 2;
|
||||
description = ''
|
||||
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; 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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
79
nix/host-modules/hive-c0re/theme.nix
Normal file
79
nix/host-modules/hive-c0re/theme.nix
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Stylix theme integration (zero-op auto-detect). When the operator's
|
||||
# host config has stylix enabled, generate a base16 `colors.css` from
|
||||
# its palette and overlay it onto the bundled frontend dist so the
|
||||
# dashboard re-themes with no operator action and no npm/esbuild
|
||||
# rebuild (a pure file-copy over the prebuilt dist). `colors.css` is
|
||||
# the entire swap contract — `theme.css` derives every semantic var
|
||||
# from the 16 base16 slots (see docs/web-ui/css-vars.md). The guarded
|
||||
# access makes this a clean no-op when stylix isn't imported into the
|
||||
# host config. Exposed as the read-only `c0re.servedFrontend` option.
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.services.hyperhive.c0re;
|
||||
stylixThemeColors =
|
||||
if (config.stylix.enable or false) && ((config.lib.stylix or { }) ? colors) then
|
||||
config.lib.stylix.colors.withHashtag
|
||||
else
|
||||
null;
|
||||
themedColorsCss =
|
||||
c:
|
||||
pkgs.writeText "hyperhive-colors.css" ''
|
||||
:root {
|
||||
--base00: ${c.base00};
|
||||
--base01: ${c.base01};
|
||||
--base02: ${c.base02};
|
||||
--base03: ${c.base03};
|
||||
--base04: ${c.base04};
|
||||
--base05: ${c.base05};
|
||||
--base06: ${c.base06};
|
||||
--base07: ${c.base07};
|
||||
--base08: ${c.base08};
|
||||
--base09: ${c.base09};
|
||||
--base0A: ${c.base0A};
|
||||
--base0B: ${c.base0B};
|
||||
--base0C: ${c.base0C};
|
||||
--base0D: ${c.base0D};
|
||||
--base0E: ${c.base0E};
|
||||
--base0F: ${c.base0F};
|
||||
}
|
||||
'';
|
||||
# Overlay the generated colors.css onto both dist subtrees. Both the
|
||||
# dashboard (served by hive-c0re via HIVE_STATIC_DIR) and the agent UIs
|
||||
# (served by the gateway from HIVE_AGENT_FRONTEND_DIR — static files
|
||||
# straight from the store) read their colors.css from this host-side
|
||||
# tree, so swapping both re-themes both surfaces.
|
||||
#
|
||||
# Not covered here: an agent reached directly on its own harness web
|
||||
# server (no gateway) serves from its per-agent `mergedDist`, built in
|
||||
# the agent's own nixosSystem with no access to the host's stylix
|
||||
# colours — theming that path needs the base16 palette forwarded
|
||||
# host→agent, tracked separately.
|
||||
themedFrontend =
|
||||
c:
|
||||
pkgs.runCommand "hyperhive-frontend-themed" { } ''
|
||||
cp -r ${cfg.frontend} $out
|
||||
chmod -R u+w $out
|
||||
install -m644 ${themedColorsCss c} $out/dashboard/static/colors.css
|
||||
install -m644 ${themedColorsCss c} $out/agent/static/colors.css
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.services.hyperhive.c0re.servedFrontend = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
internal = true;
|
||||
readOnly = true;
|
||||
default = if stylixThemeColors != null then themedFrontend stylixThemeColors else cfg.frontend;
|
||||
defaultText = lib.literalExpression "<stylix-themed overlay of `frontend`>";
|
||||
description = ''
|
||||
Internal, read-only: `frontend` re-themed with the active stylix
|
||||
palette (or `frontend` verbatim when unthemed); has `dashboard/`
|
||||
and `agent/`. Exposed so the hive-gateway module can static-serve
|
||||
`dashboard/` as an nginx root instead of proxying to hive-c0re.
|
||||
'';
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue