`services.hyperhive.agent.matrix.enable` was a second source of truth for a fact the account set already carried: after ①-③ the hive-internal `main` account is an ordinary `matrixAccounts` entry, so "does this agent have matrix" and "does this agent have an account" were the same question asked twice, with the boolean able to disagree. The option is gone and a non-empty `matrixAccounts` now gates the daemon unit, its token path-watcher and the injected `extraMcpServers.matrix` entry. That is only a real condition because `matrixAccounts.main` is itself gated: it is declared when `matrix.url != null`, never unconditionally. A `main` with no homeserver is an account the daemon can never log in as, so declaring one always would have made the signal trivially true and turned matrix on for every agent in every hive. With the URL gate, the empty set is reachable exactly for an agent the hive gave no homeserver and whose operator declared no account of its own — the state the old `enable = false` expressed. Assertions: "extras require enable" is deleted, having become the definition of the thing it checked (an external-only account with its own homeserver is now rendered rather than rejected). `main.tokenFile` stays pinned, re-guarded on `? main` instead of on the flag, since `main` is absent whenever the URL is null and an unguarded index would throw there. Both spellings of the option get `mkRemovedOptionModule`, following ../host-modules/deploy.nix's registrationTokenFile pair rather than a silent delete: the definition whose meaning changes is `false`, and left undeclared it would be ignored and hand the agent the tools its operator turned off. Failing the eval with the replacement spelling is the only outcome that cannot. module-eval gains the three arms — URL, nothing, external-only — with the middle one carrying why it exists: it is the only thing in the suite that would notice `main` becoming unconditional again. Refs #4475
327 lines
15 KiB
Nix
327 lines
15 KiB
Nix
# Shared scaffolding for every hyperhive harness container.
|
|
# `../templates/agent.nix` and `../templates/ruth.nix` both import
|
|
# agents use the same service unit regardless of which entry-point
|
|
# they came from.
|
|
#
|
|
# 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
|
|
# What an `agent.nix` still setting the dropped per-agent matrix switch is
|
|
# told. Shared by the two `mkRemovedOptionModule` entries below, one per
|
|
# spelling of the path.
|
|
matrixEnableRemoved = ''
|
|
The per-agent matrix switch is gone. Matrix is enabled for an agent exactly
|
|
when it has at least one services.hyperhive.agent.matrixAccounts entry, and
|
|
the hive-internal `main` entry is declared for you whenever
|
|
services.hyperhive.agent.matrix.url is non-null.
|
|
|
|
- `enable = true` was the default: drop the line, nothing else to do.
|
|
- `enable = false`: set services.hyperhive.agent.matrix.url = null instead
|
|
(and declare no accounts of your own). That is what leaves the account set
|
|
empty, which is now what suppresses the daemon, its path watcher and the
|
|
injected extraMcpServers.matrix entry.
|
|
|
|
See docs/tools/matrix.md.
|
|
'';
|
|
in
|
|
{
|
|
imports = [
|
|
./agent-service.nix
|
|
./bash-env.nix
|
|
./claude-settings.nix
|
|
./dashboard-links.nix
|
|
./docs.nix
|
|
./forge.nix
|
|
./frontend.nix
|
|
./github.nix
|
|
./logs.nix
|
|
./matrix.nix
|
|
./mcp.nix
|
|
./network.nix
|
|
./otel.nix
|
|
./packages.nix
|
|
./queue.nix
|
|
./renamed-options.nix
|
|
./user.nix
|
|
./screen.nix
|
|
./weston-vnc.nix
|
|
(lib.mkRemovedOptionModule [ "hyperhive" "web" "useUnixSocket" ] ''
|
|
Unix socket mode is always enabled for all agents. Remove the
|
|
setting from your agent.nix.
|
|
'')
|
|
(lib.mkRemovedOptionModule [ "hyperhive" "allowedBashPatterns" ] ''
|
|
The built-in Bash tool is fully disabled; agents use
|
|
mcp__bash__run instead. Remove the setting from your agent.nix.
|
|
'')
|
|
# The dropped per-agent matrix switch. A removal, not a silent delete,
|
|
# because the definition that *changes meaning* is `false`: left undeclared
|
|
# it would be ignored and the agent would quietly gain the matrix tools its
|
|
# operator turned off. Failing the eval with the replacement spelling is the
|
|
# only outcome that cannot do that.
|
|
#
|
|
# Both spellings, for the same reason ../host-modules/deploy.nix lists both
|
|
# of `swarm|deploy.matrix.registrationTokenFile`: an existing agent.nix may
|
|
# carry either the pre-rename `hyperhive.*` path or the current one, and only
|
|
# the one it actually carries will fire. The rename shim that used to bridge
|
|
# them is gone from ./renamed-options.nix — a rename pointing at an
|
|
# undeclared option breaks evaluation for everyone, removed or not.
|
|
(lib.mkRemovedOptionModule [ "hyperhive" "matrix" "enable" ] matrixEnableRemoved)
|
|
(lib.mkRemovedOptionModule [ "services" "hyperhive" "agent" "matrix" "enable" ] matrixEnableRemoved)
|
|
];
|
|
|
|
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.
|
|
'';
|
|
};
|
|
|
|
options.services.hyperhive.agent.claudeCodePath = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "/nix/store/…-claude-code-2.1.220";
|
|
description = ''
|
|
Store path of the `claude-code` this agent runs, or `null` (the
|
|
default) to use the `claude-code` from the container's own
|
|
nixpkgs.
|
|
|
|
Set by the generated meta flake when the operator sets
|
|
`services.hyperhive.c0re.claudeCodePackage` host-side, so a hive
|
|
can run a claude built from a *different* nixpkgs than the one
|
|
its agents evaluate against — a release channel can trail
|
|
unstable by weeks on this one package. It arrives as a path and
|
|
not a package because agents share the host's `/nix/store`, so
|
|
the build is already reachable here with its whole closure and
|
|
has nothing to travel.
|
|
|
|
When set, `claude` on PATH is a symlink to this path's
|
|
`bin/claude` and the container's own `claude-code` is dropped, so
|
|
there is only ever one claude in the container. Note that neither
|
|
the symlink nor anything else in the container's closure *refers*
|
|
to the target — keeping it alive is the host's job, see
|
|
`services.hyperhive.c0re.claudeCodePackage`.
|
|
'';
|
|
};
|
|
|
|
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
|
|
]
|
|
++ [
|
|
(
|
|
if config.services.hyperhive.agent.claudeCodePath == null then
|
|
pkgs.claude-code
|
|
else
|
|
# Host-pinned claude: a symlink farm around a path the
|
|
# container was handed as text. It has to be a derivation —
|
|
# `environment.systemPackages` coerces a store-path *string*
|
|
# with `toDerivation`, i.e. `builtins.storePath`, which pure
|
|
# evaluation rejects (`systemd.services.*.path` does the same,
|
|
# which is why the harness gets this via PATH like everything
|
|
# else rather than a unit-level entry). Interpolating the path
|
|
# into the builder is just text, so it evaluates anywhere.
|
|
#
|
|
# The symlink registers no store reference — the target isn't
|
|
# among this derivation's inputs, so nothing here keeps the
|
|
# binary alive. That is deliberate and it is the host's job:
|
|
# see `services.hyperhive.c0re.claudeCodePackage`.
|
|
pkgs.runCommandLocal "claude-code-pinned" { } ''
|
|
mkdir -p "$out/bin"
|
|
ln -s ${config.services.hyperhive.agent.claudeCodePath}/bin/claude "$out/bin/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.
|
|
# HIVE_CONTEXT_WINDOW_TOKENS_* are injected by the meta flake from the
|
|
# host-level `services.hyperhive.c0re.contextWindowTokens` option — not
|
|
# set here.
|
|
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";
|
|
};
|
|
}
|