hyperhive/nix/host-modules/hive-c0re/options.nix
atlas e407fa93df feat(#2754): hive-wide CPUWeight= / IOWeight= for agent containers
`CPUQuota=`/`MemoryMax=` are hard caps: they throttle an agent even when
the host is idle, so they are the wrong tool for "be polite under
contention". The cgroup v2 relative shares are, and neither was wired.

Adds `services.hyperhive.{agentCpuWeight,agentIoWeight}` (1..=10000,
default 80) threaded through the existing drop-in path: HiveEnv ->
write_dropins -> WriteResourceLimits -> hyperhive-limits.conf, next to
the caps already there. Hive-wide only, as the operator scoped it on the
issue: no per-agent override, no resource-limits.json field, no
dashboard form.

The default of 80 is below the kernel's 100, so agent containers yield
to everything *not* on this drop-in path -- host services and the infra
containers (hive-ci, hive-forge, hive-gateway, hive-matrix). It does not
rank agents against each other; they all carry the same weight.

`WriteResourceLimits` gains two `#[serde(default)]` fields, and the
writer treats weight 0 as "not configured" and omits the line, so an
older hive-c0re talking to a newer hive-priv still produces the exact
pre-weights drop-in. The body is extracted into `limits_dropin_body` so
that is covered by a test rather than asserted by eye.
2026-07-27 10:25:30 +02:00

379 lines
15 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.
{
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) 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 = ''
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`.
'';
};
agentCpuWeight = lib.mkOption {
type = lib.types.ints.between 1 10000;
default = 80;
example = 50;
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-ci`, `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.
'';
};
agentIoWeight = lib.mkOption {
type = lib.types.ints.between 1 10000;
default = 80;
example = 50;
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.
'';
};
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/boundary.md`.
Keep the list to trusted operators. Empty (the default) keeps the
socket root-only, as before.
'';
};
};
}