Agents run whatever `claude-code` the meta flake's `nixpkgs` resolves to, and that is normally a release channel. This one package moves fast enough that stable trails unstable by weeks — 26.05 is on 2.1.187 while unstable carries 2.1.220 — and an agent cannot fix it for itself: it only ever sees the single nixpkgs hive-c0re injects, so an `agent.nix` has no other tree to reach for. New host option `services.hyperhive.c0re.claudeCodePackage` takes the package directly and rides the existing `hyperhiveDocs` threading path — serveConfigJson -> HiveEnv -> render_flake — to reach each agent as `hyperhive.claudeCodePath`. Null (the default) is today's behaviour. What travels is the store *path*, as a plain string literal, not a flake input: containers share the host's `/nix/store`, so the build is already reachable inside them with its whole closure and has nothing to travel. An input would be worse than useless — a `path:/nix/store/<pkg>` input is re-copied as a reference-less `-source`, which strips exactly the closure the binary needs. The catch is that a path written into a generated flake is text, so nothing in the container's closure keeps the binary alive. The host does that instead, and gets it for free: the package is interpolated into `/etc/hyperhive/serve.json`, `builtins.toJSON` preserves string context, so the /etc entry references it and the system closure gc-roots it for as long as that generation is the one the agents were rendered from. An assertion pins that property, because losing the context is invisible at eval and at deploy — it would surface only as every agent failing to spawn `claude` whenever the next gc ran. Container side wraps the path in a symlink farm rather than putting it on PATH directly: `systemd.services.<name>.path` and `environment.systemPackages` both coerce a store-path *string* through `lib.toDerivation`, i.e. `builtins.storePath`, which pure evaluation rejects. Interpolating the path into a builder is just text and evaluates anywhere. `claude-code` drops out of systemPackages when a pin is set, so there is exactly one claude in the container. Refs #2693
423 lines
18 KiB
Nix
423 lines
18 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.
|
|
'';
|
|
};
|
|
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 runs, or `null` (the
|
|
default) to leave each agent on the `claude-code` from its own
|
|
nixpkgs — i.e. whatever `nixpkgsFlake` resolves to.
|
|
|
|
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 writes the path into
|
|
each agent's generated flake as a plain string literal (a bare
|
|
path fed to `lib.types.package` would run `builtins.storePath`,
|
|
which is illegal under pure evaluation) and the agent module
|
|
puts its `bin/` on the harness's PATH.
|
|
|
|
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 for exactly as long as that generation is the one
|
|
the agents were rendered from. The cost is that
|
|
`nix-collect-garbage` cannot reclaim an old `claude-code` until
|
|
every agent has been rebuilt past it and the old generations
|
|
are gone.
|
|
'';
|
|
};
|
|
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.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-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.
|
|
|
|
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/boundary.md`.
|
|
Keep the list to trusted operators. Empty (the default) keeps the
|
|
socket root-only, as before.
|
|
'';
|
|
};
|
|
};
|
|
}
|