nix: move the agent option namespace under services.hyperhive.agent

Every per-agent harness option lived at the top-level `hyperhive.*` while
the host tier has always been `services.hyperhive.*`. Move all 52 agent-tier
option leaves (33 top-level names across 16 modules) to
`services.hyperhive.agent.*`, repoint every read, and keep existing agent
configs evaluating through one `mkRenamedOptionModule` per old leaf path in
the new nix/agent-modules/renamed-options.nix.

The shims are per leaf rather than per namespace: `user`, `mcp`, `otel`,
`queue`, `docs`, `forge`, `frontend`, `github`, `gui`, `logs`, `matrix` and
`cargo` are plain attrsets of declarations, not submodule-typed options, so
a parent-path rename would not reach their children. Three read-only
options (`frontend.mergedDist`, `queue.clientIdFile`,
`queue.clientSecretFile`) deliberately get no shim — a rename contributes a
definition, which a read-only option refuses; the exclusions are commented
in place.

Refs #4473
This commit is contained in:
atlas 2026-09-17 19:29:17 +02:00
commit 3662eda440
21 changed files with 531 additions and 290 deletions

View file

@ -10,13 +10,13 @@
...
}:
let
userName = config.hyperhive.user.name;
userName = config.services.hyperhive.agent.user.name;
# This container's own effective `MemoryMax=` in bytes, baked in per
# agent by meta.rs's flake render — see
# `hyperhive.claudeMemoryMaxBytes` in ./claude-settings.nix. `null`
# `services.hyperhive.agent.claudeMemoryMaxBytes` in ./claude-settings.nix. `null`
# when the cap is `infinity` or a RAM percentage, i.e. when the module
# has no byte count to size anything against.
containerMemoryMaxBytes = config.hyperhive.claudeMemoryMaxBytes;
containerMemoryMaxBytes = config.services.hyperhive.agent.claudeMemoryMaxBytes;
# Two thirds of the container's cap, as the soft ceiling on everything
# the subagent daemon runs. The daemon spawns nested `claude` sessions
# as plain children, so its cgroup already *is* the "all subagents"
@ -32,7 +32,7 @@ let
subagentMemoryHigh = containerMemoryMaxBytes * 2 / 3;
in
{
options.hyperhive.allowedRecipients = lib.mkOption {
options.services.hyperhive.agent.allowedRecipients = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
@ -58,7 +58,7 @@ in
'';
};
options.hyperhive.extraMcpServers = lib.mkOption {
options.services.hyperhive.agent.extraMcpServers = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
@ -173,14 +173,14 @@ in
whatever brought the new agent.nix into deployed/*).
The `bash` entry above is illustrative only it's auto-injected
below (`hyperhive.extraMcpServers.bash` via `lib.mkDefault`) already
tracking `hyperhive.mcp.bashHttpPort`, so overriding it directly
below (`services.hyperhive.agent.extraMcpServers.bash` via `lib.mkDefault`) already
tracking `services.hyperhive.agent.mcp.bashHttpPort`, so overriding it directly
with a hardcoded port (as shown) is unusual and will drift if
`bashHttpPort` is changed separately; bump `bashHttpPort` instead.
'';
};
options.hyperhive.mcp.httpPort = lib.mkOption {
options.services.hyperhive.agent.mcp.httpPort = lib.mkOption {
type = lib.types.port;
default = 8790;
example = 8791;
@ -197,7 +197,7 @@ in
`initialize`/`tools-list` completed, stranding the agent with `No
such tool` the http endpoint eliminates that). `matrix` stays a
stdio bridge; `bash` runs its own persistent http listener (see
`hyperhive.mcp.bashHttpPort`).
`services.hyperhive.agent.mcp.bashHttpPort`).
Bound loopback-only; the rmcp streamable-http transport's default
`allowed_hosts` (`localhost` / `127.0.0.1` / `::1`) rejects Host
@ -220,24 +220,24 @@ in
'';
};
options.hyperhive.mcp.bashHttpPort = lib.mkOption {
options.services.hyperhive.agent.mcp.bashHttpPort = lib.mkOption {
type = lib.types.port;
default = 8791;
example = 8792;
description = ''
Loopback port `hive-bash-daemon` serves its MCP tools
(`run`/`status`/`kill`) on. Same shape as `hyperhive.mcp.httpPort`
(`run`/`status`/`kill`) on. Same shape as `services.hyperhive.agent.mcp.httpPort`
for the built-in surface: HTTP is the *sole* transport (no stdio
bridge the daemon that owns the subprocess runner serves the MCP
tools directly in-process), `Restart = "always"` keeps the listener
self-healing, and loopback-only binding means no auth token is
needed (same `allowed_hosts` reasoning as `hyperhive.mcp.httpPort`).
needed (same `allowed_hosts` reasoning as `services.hyperhive.agent.mcp.httpPort`).
Safe as a single fixed default across all agents (private
per-container network namespace see docs/networking/network.md).
'';
};
options.hyperhive.mcp.subagentHttpPort = lib.mkOption {
options.services.hyperhive.agent.mcp.subagentHttpPort = lib.mkOption {
type = lib.types.port;
default = 8793;
example = 8794;
@ -247,7 +247,7 @@ in
`hive-subagent-mcp`) a subagent spawns a full nested `claude`
process, a much heavier capability than a bash command, worth its own
deployable/restartable unit. Same shape/reasoning as
`hyperhive.mcp.bashHttpPort` otherwise: sole transport, self-healing
`services.hyperhive.agent.mcp.bashHttpPort` otherwise: sole transport, self-healing
restart, loopback-only so no auth token is needed. Shipped default-on
for every agent today, same as `bash` expected to become a real
opt-in capability gate later, not yet.
@ -263,22 +263,22 @@ in
assertions =
lib.mapAttrsToList (name: spec: {
assertion = spec.type != "stdio" || spec.command != null;
message = "hyperhive.extraMcpServers.${name}: type = \"stdio\" requires `command` to be set";
}) config.hyperhive.extraMcpServers
message = "services.hyperhive.agent.extraMcpServers.${name}: type = \"stdio\" requires `command` to be set";
}) config.services.hyperhive.agent.extraMcpServers
++ lib.mapAttrsToList (name: spec: {
assertion = spec.type != "http" || spec.url != null;
message = "hyperhive.extraMcpServers.${name}: type = \"http\" requires `url` to be set";
}) config.hyperhive.extraMcpServers;
message = "services.hyperhive.agent.extraMcpServers.${name}: type = \"http\" requires `url` to be set";
}) config.services.hyperhive.agent.extraMcpServers;
# Auto-inject the built-in bash MCP server — always present, every
# agent needs bash tools. `lib.mkDefault` so the operator's own
# agent.nix can override the entry. (The matrix sibling lives in
# ./matrix.nix, gated on hyperhive.matrix.enable.) `hive-bash-daemon`
# ./matrix.nix, gated on services.hyperhive.agent.matrix.enable.) `hive-bash-daemon`
# serves its MCP tools directly over streamable-http (no stdio bridge,
# no round-trip socket) — see the `hive-bash-daemon` service below.
hyperhive.extraMcpServers.bash = lib.mkDefault {
services.hyperhive.agent.extraMcpServers.bash = lib.mkDefault {
type = "http";
url = "http://127.0.0.1:${toString config.hyperhive.mcp.bashHttpPort}/mcp";
url = "http://127.0.0.1:${toString config.services.hyperhive.agent.mcp.bashHttpPort}/mcp";
allowedTools = [ "*" ];
};
@ -287,21 +287,22 @@ in
# later" — not gated behind an enable option yet, unlike `matrix.nix`'s
# pattern). `lib.mkDefault` so an agent.nix can still override/disable
# the entry in the meantime.
hyperhive.extraMcpServers.subagent = lib.mkDefault {
services.hyperhive.agent.extraMcpServers.subagent = lib.mkDefault {
type = "http";
url = "http://127.0.0.1:${toString config.hyperhive.mcp.subagentHttpPort}/mcp";
url = "http://127.0.0.1:${toString config.services.hyperhive.agent.mcp.subagentHttpPort}/mcp";
allowedTools = [ "*" ];
};
environment.etc."hyperhive/extra-mcp.json".text = builtins.toJSON config.hyperhive.extraMcpServers;
environment.etc."hyperhive/extra-mcp.json".text =
builtins.toJSON config.services.hyperhive.agent.extraMcpServers;
environment.etc."hyperhive/send-allow.json".text =
builtins.toJSON config.hyperhive.allowedRecipients;
builtins.toJSON config.services.hyperhive.agent.allowedRecipients;
# Bash task runner daemon — long-running process that owns subprocess
# monitoring + completion wake signals, and serves the MCP tools
# (`run`/`status`/`kill`) directly over streamable-http on
# `hyperhive.mcp.bashHttpPort` — no stdio bridge, no per-turn spawn.
# `services.hyperhive.agent.mcp.bashHttpPort` — no stdio bridge, no per-turn spawn.
systemd.services.hive-bash-daemon = {
description = "bash task runner + MCP daemon for hive-bash";
wantedBy = [ "multi-user.target" ];
@ -334,7 +335,7 @@ in
# deriving a fallback, since every service here always gets it.
};
serviceConfig = {
ExecStart = "${config.hyperhive.packages.hive-bash-daemon}/bin/hive-bash-daemon --http 127.0.0.1:${toString config.hyperhive.mcp.bashHttpPort}";
ExecStart = "${config.services.hyperhive.agent.packages.hive-bash-daemon}/bin/hive-bash-daemon --http 127.0.0.1:${toString config.services.hyperhive.agent.mcp.bashHttpPort}";
SyslogIdentifier = "hive-bash-daemon";
# `always` (not `on-failure`): since the MCP tools are served
# in-process now, a down window is total loss of bash tools with
@ -349,7 +350,7 @@ in
# Subagent task runner daemon — independent of `hive-bash-daemon` (own
# crate, own process): spawns nested claude sessions on request, serves
# the `start`/`continue`/`status`/`interrupt` MCP tools directly over
# streamable-http on `hyperhive.mcp.subagentHttpPort`. The same port also
# streamable-http on `services.hyperhive.agent.mcp.subagentHttpPort`. The same port also
# serves a second, subagent-facing route (`/signal/mcp/<token>`:
# `goal_reached`/`need_help`) — not something an agent's own config points
# at: the daemon mints each subagent it spawns its own token and writes
@ -388,7 +389,7 @@ in
# `null` when the agent has no groups declared, which systemd drops
# — the same "absent" the harness itself would see.
HIVE_TOOL_GROUPS = config.systemd.services.hive-agent.environment.HIVE_TOOL_GROUPS or null;
# Same `hyperhive.availableModels` the harness's own assertions gate
# Same `services.hyperhive.agent.availableModels` the harness's own assertions gate
# the primary session's model against, so a subagent can't be spawned
# on a model the operator didn't make available to this agent. The
# option renders into the *global* environment for the web UI's
@ -398,16 +399,16 @@ in
# a safety rail, not a security boundary), matching the harness
# assertion that an empty list waives too.
HIVE_AVAILABLE_MODELS =
if config.hyperhive.availableModels == [ ] then
if config.services.hyperhive.agent.availableModels == [ ] then
null
else
lib.concatStringsSep "," config.hyperhive.availableModels;
lib.concatStringsSep "," config.services.hyperhive.agent.availableModels;
# HYPERHIVE_HARNESS_DIR / HYPERHIVE_STATE_DIR: see
# `hive-bash-daemon`'s own comment above — same global injection,
# same reasoning.
};
serviceConfig = {
ExecStart = "${config.hyperhive.packages.hive-subagent-daemon}/bin/hive-subagent-daemon --http 127.0.0.1:${toString config.hyperhive.mcp.subagentHttpPort}";
ExecStart = "${config.services.hyperhive.agent.packages.hive-subagent-daemon}/bin/hive-subagent-daemon --http 127.0.0.1:${toString config.services.hyperhive.agent.mcp.subagentHttpPort}";
SyslogIdentifier = "hive-subagent-daemon";
# `always`, same reasoning as `hive-bash-daemon`: the MCP tools are
# served in-process, so a down window is total loss of
@ -463,7 +464,7 @@ in
# with the static loose-ends from hive-c0re.
environment.HIVE_AGENT_SOCKET = "/run/hive-agent/${userName}/agent.sock";
serviceConfig = {
ExecStart = "${config.hyperhive.packages.hive-agent-mcp}/bin/hive-agent-mcp --http 127.0.0.1:${toString config.hyperhive.mcp.httpPort}";
ExecStart = "${config.services.hyperhive.agent.packages.hive-agent-mcp}/bin/hive-agent-mcp --http 127.0.0.1:${toString config.services.hyperhive.agent.mcp.httpPort}";
SyslogIdentifier = "hive-mcp-http";
# `always` (not `on-failure`): this endpoint is load-bearing — the
# sole hyperhive-MCP transport, so a down window is total