The three values that differ per host (deploy.hive-controller.dashboardPort, claudeCodePackage and otel.extraResourceAttributes) no longer go into the meta flake. hive-priv writes them into the container's /etc/hyperhive-hive-facts with the hive name and domain, and the agent reads them at boot: - HIVE_DASHBOARD_PORT reaches every unit through the hive-facts generator; the web UI still defaults to 7000 without it. - `claude` on PATH is a wrapper that runs HYPERHIVE_CLAUDE_CODE_PATH's claude when it is there, and the container's own claude-code otherwise. The host's serve.json keeps gc-rooting the pinned build. - OTEL_RESOURCE_ATTRIBUTES moves out of the units' Environment= into a boot-time generator, which appends HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES. The agent options services.hyperhive.agent.claudeCodePath (now a removed option) and otel.extraResourceAttributes go; only the meta render set them. render_flake takes no HiveEnv any more, so the memory-cap render test and render_hive_flake_at, which only existed to vary HiveEnv into the render, go with it. Refs #4857
388 lines
16 KiB
Nix
388 lines
16 KiB
Nix
# 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. The hive's own values the meta-flake render hands
|
||
# its agents are declared here too, under
|
||
# `services.hyperhive.deploy.hive-controller`.
|
||
{
|
||
pkgs,
|
||
lib,
|
||
...
|
||
}:
|
||
{
|
||
imports = [
|
||
(lib.mkRemovedOptionModule [ "services" "hyperhive" "c0re" "managerToplevel" ] ''
|
||
Every agent, ruth included, builds from `agentBaseToplevel`. Remove this definition.
|
||
'')
|
||
(lib.mkRemovedOptionModule [ "services" "hyperhive" "c0re" "hyperhiveDocs" ] ''
|
||
Agents take their reference docs from the pinned `hyperhive` input, which
|
||
carries `docs/`. Remove this definition.
|
||
'')
|
||
];
|
||
|
||
options.services.hyperhive.c0re = {
|
||
package = lib.mkOption {
|
||
type = lib.types.package;
|
||
defaultText = lib.literalExpression "hyperhive.packages.\${system}.default";
|
||
description = ''
|
||
hyperhive workspace package. Provides `/bin/hive-c0re`
|
||
(coordinator daemon) and `/bin/hivectl` (operator-facing host
|
||
CLI for ad-hoc administration + the host admin socket). 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 = ''
|
||
Store path of the hyperhive flake source (no fragment). hive-c0re
|
||
reads it to tell which revision is running, and the swarm builder
|
||
builds on it. The meta flake does not use it: that pins
|
||
`hyperhive` by forge URL and `hyperhiveRev`. Wired by
|
||
`nixosModules.default` to this flake's own filtered source.
|
||
'';
|
||
};
|
||
hyperhiveRev = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.str;
|
||
default = null;
|
||
defaultText = lib.literalMD "the hyperhive flake's own `self.rev`";
|
||
description = ''
|
||
Git rev of the hyperhive checkout this host runs. hive-c0re pins
|
||
the generated meta flake's `hyperhive` input to
|
||
`git+<forge>/hyperhive/hyperhive.git?rev=<rev>`, and `nixpkgs`
|
||
follows that input. Wired by `nixosModules.default` from
|
||
`self.rev`, which a dirty tree does not have; with `null`,
|
||
hive-c0re refuses to render the meta flake.
|
||
'';
|
||
};
|
||
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.
|
||
'';
|
||
};
|
||
nixpkgsFlake = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "path:${pkgs.path}";
|
||
defaultText = lib.literalMD "`\"path:\${pkgs.path}\"`";
|
||
description = ''
|
||
Store-path URL of the nixpkgs the swarm builder's check flake
|
||
builds on. The generated meta flake does not use it: its `nixpkgs`
|
||
follows `hyperhive/nixpkgs`.
|
||
|
||
Defaults to `"path:''${pkgs.path}"`, the nixpkgs the host NixOS
|
||
module was evaluated with.
|
||
'';
|
||
};
|
||
preBuildAgentTemplates = lib.mkOption {
|
||
type = lib.types.bool;
|
||
default = false;
|
||
example = true;
|
||
description = ''
|
||
Pre-fetch the agent-base container system closure, which every
|
||
agent builds from, 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 toplevel is 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.
|
||
'';
|
||
};
|
||
|
||
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/stats/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`.
|
||
'';
|
||
};
|
||
|
||
agentCpuWeight = lib.mkOption {
|
||
type = lib.types.nullOr (lib.types.ints.between 1 10000);
|
||
default = 80;
|
||
example = null;
|
||
description = ''
|
||
systemd `CPUWeight=` applied to every agent container via the
|
||
same drop-in as `agentCpuQuota`. This is the cgroup v2
|
||
`cpu.weight` relative share, **not** a cap: a low-weight
|
||
container still gets the whole machine when nothing else wants
|
||
it, and the weight only decides who yields under contention.
|
||
That makes it the complement of `agentCpuQuota`, which throttles
|
||
even on an idle host.
|
||
|
||
The kernel default is `100`. The hyperhive default of `80` means
|
||
agent containers yield slightly to everything that is *not* on
|
||
this drop-in path — host services and the infrastructure
|
||
containers (`hive-forge`, `hive-gateway`, `hive-matrix`), which
|
||
stay at `100`. Note this is a hive-wide
|
||
value, so it does not rank agents against *each other*: they all
|
||
share one weight.
|
||
|
||
Set to `null` to leave `CPUWeight=` out of the drop-in entirely —
|
||
the container then inherits the kernel default and the generated
|
||
unit file is identical to one from before this option existed.
|
||
'';
|
||
};
|
||
|
||
agentIoWeight = lib.mkOption {
|
||
type = lib.types.nullOr (lib.types.ints.between 1 10000);
|
||
default = 80;
|
||
example = null;
|
||
description = ''
|
||
systemd `IOWeight=` applied to every agent container via the
|
||
same drop-in as `agentCpuQuota` — the block-IO counterpart of
|
||
`agentCpuWeight`, with the same relative-share, contention-only
|
||
semantics.
|
||
|
||
Caveat: `IOWeight=` maps to the cgroup v2 `io.weight` knob, which
|
||
is only honoured when the `io.cost` (blk-iocost) controller is
|
||
enabled for the backing device, or when the device uses the BFQ
|
||
scheduler. On a host running `none`/`mq-deadline`/`kyber` without
|
||
iocost QoS configured, systemd writes the value and the kernel
|
||
ignores it — harmless, but it will measure as a no-op. Check with
|
||
`cat /sys/fs/cgroup/io.cost.qos` on the host, and set this to
|
||
`null` to omit the setting rather than write one nothing reads.
|
||
'';
|
||
};
|
||
|
||
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.
|
||
'';
|
||
};
|
||
|
||
adminUsers = lib.mkOption {
|
||
type = lib.types.listOf lib.types.str;
|
||
default = [ ];
|
||
example = [ "alice" ];
|
||
description = ''
|
||
Login users granted **sudoless `hivectl`**. Each is added to the
|
||
`hive-admin` group, which group-owns the host admin socket
|
||
(`/run/hyperhive/host.sock`, mode `0660`) — so listed users drive
|
||
`hivectl` (and thus the whole hive) without `sudo`.
|
||
|
||
This is a real privilege grant: the admin socket is *full* hive
|
||
control — spawn / kill / destroy / deploy — see `docs/trust-boundary/boundary.md`.
|
||
Keep the list to trusted operators. Empty (the default) keeps the
|
||
socket root-only, as before.
|
||
'';
|
||
};
|
||
};
|
||
|
||
# What this hive hands its own agents at container start, through
|
||
# `/etc/hyperhive-hive-facts`. Per host, so two hives may set them
|
||
# differently.
|
||
options.services.hyperhive.deploy.hive-controller = {
|
||
claudeCodePackage = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.package;
|
||
default = null;
|
||
example = lib.literalExpression "inputs.nixpkgs-unstable.legacyPackages.x86_64-linux.claude-code";
|
||
description = ''
|
||
The `claude-code` build every agent on this hive runs, or `null` (the
|
||
default) to leave each agent on the `claude-code` from its own
|
||
nixpkgs — i.e. the nixpkgs pinned by hyperhive's own `flake.lock`.
|
||
|
||
This is the one binary the whole hive is built around, and it
|
||
moves fast enough that a release channel routinely trails
|
||
unstable by weeks on it. An agent cannot fix that for itself:
|
||
agents evaluate against the single nixpkgs hive-c0re injects,
|
||
so an `agent.nix` has no other tree to reach for. Set this from
|
||
a second nixpkgs in the host flake and every agent follows,
|
||
without moving the nixpkgs the rest of the container is built
|
||
from.
|
||
|
||
What travels into the container is the **store path**, not the
|
||
derivation: agents share the host's `/nix/store`, so the binary
|
||
and its full closure are already reachable there — nothing
|
||
needs rebuilding or copying. hive-c0re hands the path to each
|
||
container at start as `HYPERHIVE_CLAUDE_CODE_PATH`, and the agent's
|
||
`claude` on PATH runs it, or the container's own `claude-code` when
|
||
the variable is absent or names nothing. A change reaches an agent
|
||
at its next container start, not through a rebuild.
|
||
|
||
The flip side of a plain string is that nothing in the agent's
|
||
own closure refers to it, so the container cannot keep it
|
||
alive. The **host** does that instead: this package is
|
||
interpolated into `/etc/hyperhive/serve.json`, which puts it in
|
||
the host's system closure, so it is gc-rooted by the running
|
||
generation. ⚠️ An agent started under an older generation keeps
|
||
that generation's path until its next start, and only the old
|
||
generation roots it: collect old generations after restarting
|
||
the agents, or an agent still running the old path falls back to
|
||
its own `claude-code` at its next `claude` spawn.
|
||
|
||
The swarm's `swarm-claude-login` and `swarm-review-classifier`
|
||
containers, where this host runs them, follow the same pin and
|
||
keep their own nixpkgs' `claude-code` when it is `null`. They are
|
||
not agents, so pinning an older build for an agent-side
|
||
regression pins them too.
|
||
'';
|
||
};
|
||
dashboardPort = lib.mkOption {
|
||
type = lib.types.port;
|
||
default = 7000;
|
||
description = ''
|
||
TCP port this hive's hive-c0re dashboard listens on. Agents get it
|
||
at container start as `HIVE_DASHBOARD_PORT`, for their web UI's
|
||
links back to the dashboard.
|
||
'';
|
||
};
|
||
};
|
||
}
|