HIVE_AGENT_NATS_URL now reaches each container in its hive facts, from the host's deploy.hive-controller.queue.agentNatsUrl, instead of being rendered into the meta flake. The agent option services.hyperhive.agent.queue.natsUrl is removed; the harness unit imports the queue credential whenever the token endpoint is set. On deploy every agent rebuilds once (the rendered flake loses the address), and the harness reads the address from the facts file hive-priv writes before each start. Refs #4857
304 lines
13 KiB
Nix
304 lines
13 KiB
Nix
# Shared scaffolding for every hyperhive harness container.
|
|
# `../templates/agent.nix`, the one entry point every agent builds from,
|
|
# imports it.
|
|
#
|
|
# This is the core module: container plumbing (boot/nix/nixpkgs),
|
|
# base tooling, and the cross-cutting `services.hyperhive.agent.icon` option. Each
|
|
# feature lives in its own sibling module (imported below) that
|
|
# declares its own `services.hyperhive.agent.*` options + config.
|
|
{
|
|
pkgs,
|
|
lib,
|
|
config,
|
|
# Flake inputs routed through _module.args by the agent flake.nix.
|
|
# Default to {} so the module evaluates cleanly even when the agent
|
|
# flake doesn't set up the routing pattern (e.g. during standalone
|
|
# nixos-rebuild without a flake wrapper).
|
|
flakeInputs ? { },
|
|
...
|
|
}:
|
|
let
|
|
hiveNameRemoved = ''
|
|
An agent reads its hive's name at runtime from /etc/hyperhive-hive-facts,
|
|
which the hive writes into every container, so no agent build sets it.
|
|
Remove the setting.
|
|
'';
|
|
memoryMaxBytesRemoved = ''
|
|
An agent reads its memory cap at boot from /run/hive/memory-max-bytes,
|
|
which the hive writes before every start, so no agent build sets it.
|
|
Remove the setting.
|
|
'';
|
|
claudeCodePathRemoved = ''
|
|
An agent reads the claude-code its hive pins at runtime, from
|
|
HYPERHIVE_CLAUDE_CODE_PATH in /etc/hyperhive-hive-facts, so no agent build
|
|
sets it. Pin it host-side with
|
|
services.hyperhive.deploy.hive-controller.claudeCodePackage.
|
|
'';
|
|
queueRemoved = ''
|
|
The harness reads the queue's address at runtime from HIVE_AGENT_NATS_URL,
|
|
in /etc/hyperhive-hive-facts, and authenticates with the
|
|
agent's own secret from the swarm secret store, so no agent build sets a
|
|
queue option. Remove the setting.
|
|
'';
|
|
|
|
# `claude` on PATH. A hive that pins a claude-code
|
|
# (`services.hyperhive.deploy.hive-controller.claudeCodePackage`) hands its
|
|
# store path over at container start as `HYPERHIVE_CLAUDE_CODE_PATH`, and
|
|
# the host's system closure keeps it alive: nothing in this container
|
|
# references it. With no pin, or a path that is not there, this container's
|
|
# own claude-code runs; a pin that is set but gone (collected after a bump
|
|
# while this container still holds the old facts) says so on stderr, since
|
|
# the two builds can differ. `exec -a claude` keeps the bare name in argv[0],
|
|
# which is how the harness finds its claude child (`find_claude_child`).
|
|
ownClaude = import ../claude-code-no-alsa.nix lib pkgs.claude-code;
|
|
claude = pkgs.writeShellScriptBin "claude" ''
|
|
pinned=''${HYPERHIVE_CLAUDE_CODE_PATH:-}
|
|
if [ -n "$pinned" ] && [ -x "$pinned/bin/claude" ]; then
|
|
exec -a claude "$pinned/bin/claude" "$@"
|
|
fi
|
|
if [ -n "$pinned" ]; then
|
|
echo "claude: $pinned/bin/claude, the hive's pinned claude-code, is missing; running this container's own" >&2
|
|
fi
|
|
exec -a claude ${ownClaude}/bin/claude "$@"
|
|
'';
|
|
in
|
|
{
|
|
imports = [
|
|
./agent-service.nix
|
|
./bao.nix
|
|
./bash-env.nix
|
|
./claude-settings.nix
|
|
./dashboard-links.nix
|
|
./docs.nix
|
|
./forge.nix
|
|
./forge-accounts.nix
|
|
./forge-token.nix
|
|
./frontend.nix
|
|
./github.nix
|
|
./github-token.nix
|
|
./hive-ca.nix
|
|
./hive-facts.nix
|
|
./logs.nix
|
|
./matrix.nix
|
|
./mcp.nix
|
|
./memory-cap.nix
|
|
./metrics.nix
|
|
./network.nix
|
|
./otel.nix
|
|
./otel-removed.nix
|
|
./packages.nix
|
|
./queue-identity.nix
|
|
./user.nix
|
|
./screen.nix
|
|
./weston-vnc.nix
|
|
# The hive supplies both at runtime; a definition fails the eval, naming
|
|
# where the value comes from.
|
|
(lib.mkRemovedOptionModule [ "services" "hyperhive" "agent" "hiveName" ] hiveNameRemoved)
|
|
(lib.mkRemovedOptionModule [
|
|
"services"
|
|
"hyperhive"
|
|
"agent"
|
|
"claudeMemoryMaxBytes"
|
|
] memoryMaxBytesRemoved)
|
|
(lib.mkRemovedOptionModule [
|
|
"services"
|
|
"hyperhive"
|
|
"agent"
|
|
"claudeCodePath"
|
|
] claudeCodePathRemoved)
|
|
(lib.mkRemovedOptionModule [ "services" "hyperhive" "agent" "queue" "natsUrl" ] queueRemoved)
|
|
(lib.mkRemovedOptionModule [ "services" "hyperhive" "agent" "queue" "tokenEndpoint" ] queueRemoved)
|
|
];
|
|
|
|
options.services.hyperhive.agent.icon = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
example = lib.literalExpression "./icon.svg";
|
|
description = ''
|
|
Path to an SVG file used as this agent's icon — shown on the
|
|
dashboard and the per-agent web UI (header + favicon). Commit
|
|
the SVG into the agent's config repo next to `agent.nix` and
|
|
reference it as a relative path (`./icon.svg`).
|
|
|
|
When null (the default), `GET /icon` on the per-agent web port
|
|
404s and consumers (dashboard, this agent's own page header)
|
|
fall back client-side to the frontend-bundled `/favicon.svg`
|
|
rather than a server-resolved default.
|
|
'';
|
|
};
|
|
|
|
config = {
|
|
assertions = [
|
|
# Guard the inputs-routed-as-output pattern: the agent flake.nix is
|
|
# expected to set `_module.args.flakeInputs = builtins.removeAttrs inputs ["self"]`.
|
|
# If `self` leaks into flakeInputs the agent gets a spurious attrset
|
|
# entry that can shadow real inputs and is almost certainly a bug.
|
|
# Guard with `or {}` so standalone evaluation stays clean when
|
|
# flakeInputs is absent from _module.args.
|
|
{
|
|
assertion = !(builtins.hasAttr "self" (config._module.args.flakeInputs or { }));
|
|
message = ''
|
|
hyperhive: `flakeInputs` must not contain "self".
|
|
In your agent flake.nix, use:
|
|
_module.args.flakeInputs = builtins.removeAttrs inputs [ "self" ];
|
|
'';
|
|
}
|
|
# services.hyperhive.agent.icon must reference an SVG file when set.
|
|
{
|
|
assertion =
|
|
config.services.hyperhive.agent.icon == null
|
|
|| lib.hasSuffix ".svg" (toString config.services.hyperhive.agent.icon);
|
|
message = "services.hyperhive.agent.icon must point to an .svg file";
|
|
}
|
|
];
|
|
|
|
# Operator-set per-agent icon (services.hyperhive.agent.icon). When configured, the
|
|
# SVG lands at /etc/hyperhive/icon.svg; the harness serves it at
|
|
# GET /icon, 404ing when absent (client-side fallback, no
|
|
# server-side default). Consumed by forge-avatar-sync (./forge.nix)
|
|
# and the matrix avatar sync (./matrix.nix) too.
|
|
environment.etc."hyperhive/icon.svg" = lib.mkIf (config.services.hyperhive.agent.icon != null) {
|
|
source = config.services.hyperhive.agent.icon;
|
|
};
|
|
|
|
boot.isNspawnContainer = true;
|
|
|
|
# Use a disk-backed /tmp instead of the default tmpfs so large scratch
|
|
# writes (nix-develop shells, cargo build dirs, multi-GB downloads) land
|
|
# on disk rather than eating container RAM. The tmpfs default mounts
|
|
# ~3.2 GB of RAM per container; disk-backed /tmp is effectively unlimited
|
|
# and cheaper for agents that do heavy build work.
|
|
#
|
|
# cleanOnBoot defaults to false in nixpkgs — set it explicitly so /tmp is
|
|
# cleared on each container start (D! tmpfiles rule), preserving the
|
|
# ephemeral-per-boot semantics agents expect from a tmpfs /tmp, just
|
|
# without the RAM cost.
|
|
boot.tmp.useTmpfs = false;
|
|
boot.tmp.cleanOnBoot = true;
|
|
|
|
# Every agent gets flakes + the modern `nix` CLI out of the box.
|
|
# Equivalent to passing `--extra-experimental-features 'nix-command
|
|
# flakes'` on every invocation. Agents shell out to `nix build` /
|
|
# `nix flake` constantly (devshells, ad-hoc evals, fetching their
|
|
# own MCP-server flakes); without this they hit the "experimental
|
|
# feature not enabled" wall on the first try.
|
|
nix.settings.experimental-features = [
|
|
"nix-command"
|
|
"flakes"
|
|
];
|
|
|
|
# `lib.mkForce` overrides nixpkgs's normal-priority `false` so
|
|
# in-container `nix build` invocations fall back to unsandboxed
|
|
# local builds rather than failing on the missing user-namespace.
|
|
# See `docs/process/gotchas.md::Containerized nix-daemon needs
|
|
# sandbox-fallback = true` + `docs/trust-boundary/security.md` for the rationale.
|
|
#
|
|
# Note: with NIX_REMOTE=daemon below this becomes a no-op for the
|
|
# common case — daemon-routed builds run on the host where sandboxing
|
|
# works. It stays as a belt-and-suspenders fallback for any context
|
|
# that bypasses the daemon (e.g. direct nix-store invocations).
|
|
nix.settings.sandbox-fallback = lib.mkForce true;
|
|
|
|
# Fall back to a local build when a remote builder can't be reached,
|
|
# instead of hard-failing the whole invocation.
|
|
#
|
|
# This is NOT redundant with the host daemon's own setting, and that
|
|
# is the subtle part: `fallback` (protocol `tryFallback`) is a *client*
|
|
# option. Every nix client transmits it to the daemon on connect, so
|
|
# the client — i.e. this container — decides whether a failed remote
|
|
# dispatch may degrade to a local build, even though the build itself
|
|
# executes on the host daemon under NIX_REMOTE=daemon below. Contrast
|
|
# `builders`, which is genuinely daemon-side and which an untrusted
|
|
# client cannot override. Without this, a container inherits nix's
|
|
# default `false`, so a momentarily unreachable remote builder kills
|
|
# the invocation while the exact same build started on the host
|
|
# succeeds.
|
|
nix.settings.fallback = true;
|
|
|
|
# Route ALL nix invocations in this container through the host
|
|
# nix-daemon socket, regardless of whether the caller is root or
|
|
# non-root. Without this, root contexts (PID 1, systemd services
|
|
# running as root) default to store=auto which resolves to the LOCAL
|
|
# store — bypassing the shared daemon, its remote builders, and the
|
|
# host's prebuilt derivation cache, causing spurious full rebuilds.
|
|
#
|
|
# systemd.globalEnvironment sets DefaultEnvironment in systemd.conf,
|
|
# so every unit started by PID 1 inherits NIX_REMOTE=daemon.
|
|
# Non-root nix clients already default to the daemon socket, so this
|
|
# is a no-op for them; it only matters for root services that would
|
|
# otherwise silently use the local store.
|
|
systemd.globalEnvironment.NIX_REMOTE = "daemon";
|
|
|
|
# `claude-code` is unfree. Each per-agent container's nixosConfiguration
|
|
# evaluates its own `nixpkgs` instance, so the operator's host-level
|
|
# `nixpkgs.config.allowUnfreePredicate` does not propagate into here —
|
|
# we have to allow it inside the container's config as well.
|
|
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "claude-code" ];
|
|
|
|
# Core tooling every agent gets. Per-bin split packages (see
|
|
# nix/packages/default.nix + ./packages.nix) rather than the full
|
|
# `hyperhive` bundle — that bundle also carries `hivectl` (a
|
|
# host-admin CLI that dials the *host* admin socket — useless and
|
|
# unreachable from inside a container — wrapped with
|
|
# `wireguard-tools` for `hivectl wg`). The daemon/harness/MCP bins
|
|
# the harness execs (hive-agent{,-mcp}, hive-bash-daemon,
|
|
# hive-subagent-daemon, hive-matrix-daemon, hive-matrix-mcp) are
|
|
# wired via their own
|
|
# ExecStart/command lines in the sibling modules — they don't need
|
|
# to be on PATH too. Only this one is actually looked up on PATH
|
|
# by claude/shell code inside the container:
|
|
# `hive-metric` (agent-emitted custom metrics CLI,
|
|
# docs/scheduler/observability.md).
|
|
environment.systemPackages = [
|
|
config.services.hyperhive.agent.packages.hive-metric
|
|
]
|
|
++ [
|
|
claude
|
|
]
|
|
++ (with pkgs; [
|
|
bashInteractive
|
|
coreutils-full
|
|
# procps for kill — used by the web UI's /api/cancel and /api/logout
|
|
# to SIGINT the harness-spawned claude child found via a /proc scan
|
|
# (see hive-agent::web_ui::find_claude_child).
|
|
procps
|
|
# jq: JSON processing in shell — useful for parsing API responses,
|
|
# forge REST calls, sqlite output, etc.
|
|
jq
|
|
# curl: HTTP client for forge REST API and other web requests.
|
|
curl
|
|
]);
|
|
|
|
# HIVE_ASSETS_DIR points at the project's static runtime assets
|
|
# (branding + claude prompts; see `nix/packages/assets.nix`). Set
|
|
# here so both the harness binary and any user-shell `cargo run`
|
|
# inside the container resolve them from the same path.
|
|
# SHELL must be set so claude's Bash tool finds a POSIX shell.
|
|
environment.variables = {
|
|
HIVE_ASSETS_DIR = "${config.services.hyperhive.agent.packages.assets}/share/hyperhive";
|
|
SHELL = "${pkgs.bashInteractive}/bin/bash";
|
|
# Route interactive-shell nix invocations through the host daemon.
|
|
# Redundant with /etc/profile.d/nix-daemon.sh but ensures it's set
|
|
# regardless of which profile files are sourced.
|
|
NIX_REMOTE = "daemon";
|
|
};
|
|
|
|
# Git is needed by claude's Bash tool (for the agent <-> manager config
|
|
# request flow) and by hive-c0re's own setup_applied / setup_proposed.
|
|
# The per-agent `applied/<name>/flake.nix` overrides `user.name` and
|
|
# `user.email` with the agent's identity — values here are `mkDefault`
|
|
# so the per-agent override wins without needing `mkForce`.
|
|
programs.git = {
|
|
enable = true;
|
|
config = {
|
|
user = {
|
|
name = lib.mkDefault "hyperhive";
|
|
email = lib.mkDefault "hyperhive@local";
|
|
};
|
|
init.defaultBranch = lib.mkDefault "main";
|
|
};
|
|
};
|
|
|
|
system.stateVersion = "25.11";
|
|
};
|
|
}
|