`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.
379 lines
15 KiB
Nix
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.
|
|
'';
|
|
};
|
|
};
|
|
}
|